From 81ecad2b13a413c8404c74940d99cddd81ae369c Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 14 Sep 2026 09:10:03 +0200 Subject: [PATCH 1/3] feat(agent-manager): drag sessions, worktrees, terminals, and documents into the prompt --- .../agent-manager-prompt-mention-drop.md | 5 + .../tests/unit/prompt-mention-drop.test.ts | 100 ++++++++++++++++++ .../tests/unit/use-file-mention.test.ts | 92 +++++++++++++++- .../agent-manager/InspectorTabStrip.tsx | 11 +- .../agent-manager/ProjectSidebarBody.tsx | 27 ++++- .../webview-ui/agent-manager/SidebarBody.tsx | 31 +++++- .../agent-manager/constrain-drag-x.ts | 8 +- .../webview-ui/agent-manager/section-dnd.ts | 13 +++ .../webview-ui/agent-manager/tab-drag.ts | 17 ++- .../terminal/SideTerminalPanel.tsx | 1 + .../agent-manager/worktree-references.ts | 23 +++- .../webview-ui/documents/DocumentPanel.tsx | 4 + .../src/components/chat/PromptInput.tsx | 19 +++- .../src/components/chat/SessionTabStrip.tsx | 10 +- .../webview-ui/src/components/chat/TabDnd.tsx | 27 ++++- .../src/hooks/file-mention-utils.ts | 10 ++ .../webview-ui/src/hooks/useFileMention.ts | 91 ++++++++++++++-- .../webview-ui/src/styles/prompt-input.css | 4 + .../src/utils/prompt-mention-drop.ts | 83 +++++++++++++++ 19 files changed, 550 insertions(+), 26 deletions(-) create mode 100644 .changeset/agent-manager-prompt-mention-drop.md create mode 100644 packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts diff --git a/.changeset/agent-manager-prompt-mention-drop.md b/.changeset/agent-manager-prompt-mention-drop.md new file mode 100644 index 000000000000..07db957525fd --- /dev/null +++ b/.changeset/agent-manager-prompt-mention-drop.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Mention sessions, worktrees, terminals, and open documents by dragging their tab or card into the prompt. diff --git a/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts b/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts new file mode 100644 index 000000000000..c6fbed3a1642 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts @@ -0,0 +1,100 @@ +import { afterEach, describe, expect, it } from "bun:test" +import { + beginPromptMentionDrop, + endPromptMentionDrop, + insideRect, + registerPromptMentionDrop, + type PromptMentionDrop, +} from "../../webview-ui/src/utils/prompt-mention-drop" + +const hadDoc = "document" in globalThis +const originalDoc = hadDoc ? globalThis.document : undefined +const listeners = new Set<(event: PointerEvent) => void>() + +function mockDocument() { + listeners.clear() + ;(globalThis as Record).document = { + addEventListener: (type: string, handler: (event: PointerEvent) => void) => { + if (type === "pointermove") listeners.add(handler) + }, + removeEventListener: (type: string, handler: (event: PointerEvent) => void) => { + if (type === "pointermove") listeners.delete(handler) + }, + } +} + +function restoreDocument() { + if (hadDoc) (globalThis as Record).document = originalDoc + else delete (globalThis as Record).document +} + +function move(x: number, y: number) { + for (const handler of listeners) handler({ clientX: x, clientY: y } as PointerEvent) +} + +function target() { + return { + isConnected: true, + getBoundingClientRect: () => ({ left: 100, top: 100, right: 300, bottom: 200 }), + } as unknown as HTMLElement +} + +const drop: PromptMentionDrop = { + kind: "session", + session: { id: "s1", title: "Chat", updated: 1 }, +} + +afterEach(() => { + registerPromptMentionDrop(undefined, undefined) + endPromptMentionDrop() + restoreDocument() +}) + +describe("insideRect", () => { + it("includes the edges and excludes outside points", () => { + const rect = { left: 10, top: 20, right: 30, bottom: 40 } + expect(insideRect(rect, 10, 20)).toBe(true) + expect(insideRect(rect, 30, 40)).toBe(true) + expect(insideRect(rect, 9, 20)).toBe(false) + expect(insideRect(rect, 30, 41)).toBe(false) + }) +}) + +describe("prompt mention drop", () => { + it("inserts only when the last pointer position is inside the target", () => { + mockDocument() + const inserted: PromptMentionDrop[] = [] + registerPromptMentionDrop(target(), (value) => { + inserted.push(value) + return true + }) + + beginPromptMentionDrop(drop) + move(200, 150) + expect(endPromptMentionDrop()).toBe(true) + + expect(inserted).toEqual([drop]) + }) + + it("does not insert when the pointer is outside the target", () => { + mockDocument() + const inserted: PromptMentionDrop[] = [] + registerPromptMentionDrop(target(), (value) => { + inserted.push(value) + return true + }) + + beginPromptMentionDrop(drop) + move(500, 500) + expect(endPromptMentionDrop()).toBe(false) + + expect(inserted).toEqual([]) + }) + + it("does nothing when no target is registered", () => { + mockDocument() + beginPromptMentionDrop(drop) + expect(listeners.size).toBe(0) + expect(endPromptMentionDrop()).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/use-file-mention.test.ts b/packages/kilo-vscode/tests/unit/use-file-mention.test.ts index 6b3e24f37823..24f3232c41d5 100644 --- a/packages/kilo-vscode/tests/unit/use-file-mention.test.ts +++ b/packages/kilo-vscode/tests/unit/use-file-mention.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from "bun:test" import { createRoot, createSignal } from "solid-js" import { useFileMention } from "../../webview-ui/src/hooks/useFileMention" -import { FILE_PICKER_RESULT, MODEL_RESULT, TERMINAL_RESULT } from "../../webview-ui/src/hooks/file-mention-utils" +import { + FILE_PICKER_RESULT, + MODEL_RESULT, + TERMINAL_RESULT, + type WorktreeReference, +} from "../../webview-ui/src/hooks/file-mention-utils" import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages" declare global { @@ -1925,3 +1930,88 @@ describe("useFileMention", () => { dispose.fn?.() }) }) + +describe("useFileMention reference drops", () => { + const ctx = { + postMessage: () => {}, + onMessage: () => () => {}, + } + + const worktree: WorktreeReference = { + id: "w1", + name: "Feature", + branch: "feature", + path: "/repo/worktrees/feature", + base: "main", + sessions: [{ id: "s1", title: "Chat" }], + disabled: false, + } + + const withMention = ( + text: string, + worktrees: WorktreeReference[] | undefined, + run: (mention: ReturnType, area: ReturnType) => void, + ) => { + const area = editor(text) + mockDocument(area) + const dispose: { fn?: () => void } = {} + let mention!: ReturnType + createRoot((root) => { + dispose.fn = root + mention = useFileMention( + ctx, + () => "s1", + () => false, + worktrees ? () => worktrees : undefined, + ) + }) + try { + run(mention, area) + } finally { + dispose.fn?.() + restoreDocument() + } + } + + it("inserts a worktree reference after existing text and attaches it", () => { + withMention("hello", [worktree], (mention, area) => { + mention.insertDrop({ kind: "worktree", worktree }, area, () => {}, "") + expect(area.value).toBe("hello @/repo/worktrees/feature ") + expect(mention.mentionedPaths().has(worktree.path)).toBe(true) + expect(mention.parseFileAttachments(area.value).map((file) => file.filename)).toContain("worktree-w1.txt") + }) + }) + + it("inserts a session reference and attaches it", () => { + withMention("", undefined, (mention, area) => { + mention.insertDrop({ kind: "session", session: { id: "s2", title: "My Chat", updated: 5 } }, area, () => {}, "") + expect(area.value).toBe("@My Chat ") + expect(mention.mentionedSessions().has("My Chat")).toBe(true) + expect(mention.parseFileAttachments(area.value).map((file) => file.url)).toContain("session:s2") + }) + }) + + it("inserts the terminal reference", () => { + withMention("", undefined, (mention, area) => { + expect(mention.insertDrop({ kind: "terminal" }, area, () => {}, "")).toBe(true) + expect(area.value).toBe("@terminal ") + }) + }) + + it("inserts a relative file reference from a document tab", () => { + withMention("", undefined, (mention, area) => { + expect(mention.insertDrop({ kind: "file", path: "/repo/docs/plan.md" }, area, () => {}, "/repo")).toBe(true) + expect(area.value).toBe("@docs/plan.md ") + expect(mention.mentionedPaths().has("docs/plan.md")).toBe(true) + }) + }) + + it("skips a disabled worktree reference", () => { + withMention("", [worktree], (mention, area) => { + expect( + mention.insertDrop({ kind: "worktree", worktree: { ...worktree, disabled: true } }, area, () => {}, ""), + ).toBe(false) + expect(area.value).toBe("") + }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx b/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx index 5ec8efac2fdc..483651f19461 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx @@ -7,7 +7,8 @@ import { type DragEvent, } from "@thisbeyond/solid-dnd" import { For, Show, createSignal, type Accessor, type Component, type JSX } from "solid-js" -import { ConstrainDragYAxis } from "../src/components/chat/TabDnd" +import { ConstrainDragYAxis, outsideSidePanel } from "../src/components/chat/TabDnd" +import { beginPromptMentionDrop, endPromptMentionDrop, type PromptMentionDrop } from "../src/utils/prompt-mention-drop" import { createTabFocus } from "../src/utils/tab-navigation" import { useTabScroll } from "../src/utils/tab-scroll" import { setTabWidths } from "../src/utils/tab-widths" @@ -30,6 +31,8 @@ interface Props { overlay: (id: string) => string onSelect: (id: string) => void onReorder: (from: string, to: string) => void + /** Prompt mention payload for a tab id, so the tab can be dragged to the prompt. */ + drag?: (id: string) => PromptMentionDrop | undefined action?: (api: InspectorTabStripApi) => JSX.Element } @@ -47,12 +50,18 @@ export const InspectorTabStrip: Component = (props) => { const width = event.draggable?.layout.width ?? event.draggable?.node.getBoundingClientRect().width freeze() setDragging({ id, width }) + const payload = props.drag?.(id) + if (payload) beginPromptMentionDrop(payload) } const end = () => { + endPromptMentionDrop() setDragging(undefined) release() } const over = (event: DragEvent) => { + // Once the tab leaves the side panel it is on its way to the prompt, so stop + // reordering the tabs under it. Only applies to drag-to-prompt strips. + if (props.drag && outsideSidePanel(event)) return const from = event.draggable?.id const to = event.droppable?.id if (typeof from !== "string" || typeof to !== "string") return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index 101583ae2bc1..ae99be9ed652 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -37,13 +37,15 @@ import { isGrouped, } from "./section-helpers" import { LOCAL, nextSelectionAfterDelete } from "./navigate" -import { sectionAwareDetector } from "./section-dnd" +import { outsideSidebar, sectionAwareDetector } from "./section-dnd" import { ConstrainDragXAxis } from "./constrain-drag-x" import { createProjectStore, type ProjectStore } from "./project/store" import { randomColor } from "./section-colors" import { projectSidebarOrder, projectWorktreeRow } from "./project-local-navigation" import { rootSessions } from "./project/session-filter" import { createWorktreeCompletion } from "./worktree-completion" +import { worktreeDropReference } from "./worktree-references" +import { beginPromptMentionDrop, endPromptMentionDrop } from "../src/utils/prompt-mention-drop" const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) @@ -201,10 +203,24 @@ export const ProjectSidebarBody: Component = (props) => { if (!id || !worktreeIds().has(id)) return setDragging(id) setDragOrigin(order()) + const wt = worktrees().find((item) => item.id === id) + if (wt) { + beginPromptMentionDrop({ + kind: "worktree", + worktree: worktreeDropReference( + wt, + wt.label || firstOrderedTitle(sessions(wt.id), store.tabOrder()[wt.id], wt.branch), + sessions(wt.id).map((session) => ({ id: session.id })), + ), + }) + } document.body.classList.add("am-wt-dragging-active") } const onDragOver = (event: DragEvent) => { + // Once the card leaves the sidebar it is on its way to the prompt, so stop + // reordering the list under it. + if (outsideSidebar(event.draggable)) return const from = parse("worktree", event.draggable?.id) const to = parse("worktree", event.droppable?.id) if (!from || !to || !worktreeIds().has(from) || !worktreeIds().has(to)) return @@ -218,6 +234,7 @@ export const ProjectSidebarBody: Component = (props) => { } const onDragEnd = (event: DragEvent) => { + const handled = endPromptMentionDrop() const from = parse("worktree", event.draggable?.id) const section = parse("section", event.droppable?.id) const to = parse("worktree", event.droppable?.id) @@ -225,6 +242,14 @@ export const ProjectSidebarBody: Component = (props) => { const origin = dragOrigin() setDragOrigin(undefined) document.body.classList.remove("am-wt-dragging-active") + // A drop on the prompt inserts a mention. Do not also move the worktree to + // whatever section happens to be under the pointer. + if (handled) return + // A release outside the sidebar is not a section move or list reorder. + if (outsideSidebar(event.draggable)) { + if (origin) store.setWorktreeOrder(origin) + return + } if (!from || !worktreeIds().has(from)) { if (origin) store.setWorktreeOrder(origin) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx index b1892d30e820..d1cb1ef22c5b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx @@ -23,7 +23,9 @@ import { LOCAL, adjacentHint } from "./navigate" import { applyTabOrder, reorderTabs } from "./tab-order" import { buildTopLevelItems, isGroupEnd, isGroupStart, isGrouped } from "./section-helpers" import { createWorktreeCompletion } from "./worktree-completion" -import { sectionAwareDetector } from "./section-dnd" +import { worktreeDropReference } from "./worktree-references" +import { beginPromptMentionDrop, endPromptMentionDrop } from "../src/utils/prompt-mention-drop" +import { outsideSidebar, sectionAwareDetector } from "./section-dnd" import { ConstrainDragXAxis } from "./constrain-drag-x" import { useVSCode } from "../src/context/vscode" import SectionHeader from "./SectionHeader" @@ -250,10 +252,29 @@ export const SidebarBody: Component = (props) => { const onWtDragStart = (event: DragEvent) => { const id = event.draggable?.id - if (typeof id === "string") props.setDraggingWorktree(id) + if (typeof id === "string") { + props.setDraggingWorktree(id) + const wt = sorted().find((item) => item.id === id) + if (wt) { + beginPromptMentionDrop({ + kind: "worktree", + worktree: worktreeDropReference( + wt, + props.worktreeLabel(wt), + props + .managedSessions() + .filter((session) => session.worktreeId === wt.id) + .map((session) => ({ id: session.id })), + ), + }) + } + } document.body.classList.add("am-wt-dragging-active") } const onWtDragOver = (event: DragEvent) => { + // Once the card leaves the sidebar it is on its way to the + // prompt, so stop reordering the list under it. + if (outsideSidebar(event.draggable)) return const from = event.draggable?.id const to = event.droppable?.id if (typeof from !== "string" || typeof to !== "string") return @@ -267,10 +288,16 @@ export const SidebarBody: Component = (props) => { }) } const onWtDragEnd = (event: DragEvent) => { + const handled = endPromptMentionDrop() const from = event.draggable?.id const to = event.droppable?.id props.setDraggingWorktree(undefined) document.body.classList.remove("am-wt-dragging-active") + // A drop on the prompt inserts a mention. Do not also move the + // worktree to whatever section happens to be under the pointer. + if (handled) return + // A release outside the sidebar is not a list reorder. + if (outsideSidebar(event.draggable)) return if (typeof from === "string" && typeof to === "string" && secIds().has(to)) { props.moveToSection([from], to) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/constrain-drag-x.ts b/packages/kilo-vscode/webview-ui/agent-manager/constrain-drag-x.ts index 3c9ed79504c4..3893318bb943 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/constrain-drag-x.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/constrain-drag-x.ts @@ -1,12 +1,16 @@ import { type Component, createRoot, onCleanup } from "solid-js" import { useDragDropContext, type Transformer } from "@thisbeyond/solid-dnd" -/** Lock drag movement to the Y axis (vertical-only worktree dragging). */ +/** + * Keep worktree drags from drifting left off-screen while allowing movement to + * the right, so a card can leave the sidebar and be dropped on the prompt. + * Vertical position still drives the sortable reorder animation. + */ export const ConstrainDragXAxis: Component = () => { const ctx = useDragDropContext() if (!ctx) return null const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = ctx - const xform: Transformer = { id: "constrain-x-axis", order: 100, callback: (t) => ({ ...t, x: 0 }) } + const xform: Transformer = { id: "constrain-x-axis", order: 100, callback: (t) => ({ ...t, x: Math.max(0, t.x) }) } const dispose = createRoot((d) => { onDragStart(({ draggable }) => { if (draggable) addTransformer("draggables", draggable.id as string, xform) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/section-dnd.ts b/packages/kilo-vscode/webview-ui/agent-manager/section-dnd.ts index d625f795ca80..a794ad3090bf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/section-dnd.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/section-dnd.ts @@ -33,3 +33,16 @@ export function sectionAwareDetector( return closestCenter(draggable, droppables, ctx) } } + +/** + * True once a dragged worktree card has moved right past its own bounds, which + * means it left the sidebar. The sidebar sorts by vertical position, so drag + * over must stop reordering once the card is out. Otherwise the siblings keep + * animating while the user drags toward the prompt. + */ +export function outsideSidebar(draggable: { + layout: { right: number } + transformed: { center: { x: number } } +}): boolean { + return draggable.transformed.center.x > draggable.layout.right +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/tab-drag.ts b/packages/kilo-vscode/webview-ui/agent-manager/tab-drag.ts index 63caafe96456..21131e0653f6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/tab-drag.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/tab-drag.ts @@ -3,10 +3,12 @@ import type { DragEvent } from "@thisbeyond/solid-dnd" import { LOCAL } from "./navigate" import { applyTabOrder, reorderTabs } from "./tab-order" import { isTerminalTabId, type TerminalStateControls } from "./terminal/state" +import { beginPromptMentionDrop, endPromptMentionDrop, sessionDrop } from "../src/utils/prompt-mention-drop" +import { outsideTabBar } from "../src/components/chat/TabDnd" export function createTabDrag(opts: { selection: Accessor - sessions: Accessor<{ id: string; title?: string }[]> + sessions: Accessor<{ id: string; title?: string; updatedAt?: string }[]> review: { id: string; open: Accessor; title: Accessor } order: Accessor> setOrder: Setter> @@ -43,9 +45,19 @@ export function createTabDrag(opts: { overlay, start(event: DragEvent) { const id = event.draggable?.id - if (typeof id === "string") setDragging(id) + if (typeof id !== "string") return + setDragging(id) + if (isTerminalTabId(id)) { + beginPromptMentionDrop({ kind: "terminal" }) + return + } + const session = opts.sessions().find((item) => item.id === id) + if (session) beginPromptMentionDrop(sessionDrop(session)) }, over(event: DragEvent) { + // Once the tab is below the bar it is on its way to the prompt, so stop + // reordering the tabs under it. + if (outsideTabBar(event)) return const from = event.draggable?.id const to = event.droppable?.id if (typeof from !== "string" || typeof to !== "string") return @@ -60,6 +72,7 @@ export function createTabDrag(opts: { if (terminals.length > 0) opts.terms.reorder(opts.namespace(key), terminals) }, end() { + endPromptMentionDrop() setDragging(undefined) const key = opts.selection() if (key === null) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx index 81420549957d..1e8ad45950a9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx @@ -79,6 +79,7 @@ export const SideTerminalPanel: Component = (props) => { overlay={(id) => props.state.title(id) ?? t("agentManager.tab.terminal")} onSelect={props.onSelect} onReorder={(from, to) => props.state.reorderSideDrag(props.contextKey(), from, to)} + drag={() => ({ kind: "terminal" })} renderTab={(id, api) => { const term = sides().find((item) => item.id === id) if (!term) return null diff --git a/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts b/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts index 36d5c92896f0..3dc30019e84a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts @@ -1,5 +1,5 @@ import { createEffect, createMemo, type Accessor } from "solid-js" -import type { SessionInfo } from "../src/types/messages" +import type { SessionInfo, WorktreeState } from "../src/types/messages" import type { useVSCode } from "../src/context/vscode" import type { WorktreeReference } from "../src/hooks/file-mention-utils" import type { ProjectStore } from "./project/store" @@ -51,6 +51,27 @@ export function worktreeReferences( ) } +/** + * Build the reference carried by a dragged worktree card. The sidebar already + * has the worktree state and its sessions, so the drop does not depend on the + * active project's mention list. + */ +export function worktreeDropReference( + worktree: WorktreeState, + name: string, + sessions: { id: string; title?: string }[], +): WorktreeReference { + return { + id: worktree.id, + name, + branch: worktree.branch, + path: worktree.path, + base: worktree.parentBranch, + sessions, + disabled: false, + } +} + export function createWorktreeReferences( vscode: Pick, "getState" | "setState">, state: Accessor, diff --git a/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx b/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx index 643c015ce844..b643ee83034d 100644 --- a/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx +++ b/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx @@ -271,6 +271,10 @@ export const DocumentPanel: Component = (props) => { overlay={(id) => props.tabs().find((tab) => tab.id === id)?.file ?? ""} onSelect={props.onSelect} onReorder={props.onReorder} + drag={(id) => { + const tab = props.tabs().find((item) => item.id === id) + return tab ? { kind: "file", path: tab.file } : undefined + }} renderTab={(id, api) => { const tab = props.tabs().find((item) => item.id === id)! return ( diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index b385a2db17b9..c7708a9c4006 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -3,7 +3,7 @@ * Text input with send/abort buttons, ghost-text autocomplete, and @ file mention support */ -import { createSignal, createEffect, on, For, Index, onCleanup, Show, untrack, type Component } from "solid-js" +import { createSignal, createEffect, on, onMount, For, Index, onCleanup, Show, untrack, type Component } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" @@ -43,6 +43,7 @@ import { useSpeechToTextModels } from "../../context/speech-to-text-models" import { createSpeechShortcut } from "../speech-to-text/shortcut" import { useImageAttachments, type ImageAttachment } from "../../hooks/useImageAttachments" import { convertToMentionPath, insertPathMentions } from "../../utils/path-mentions" +import { promptMentionOver, registerPromptMentionDrop } from "../../utils/prompt-mention-drop" import { SessionMentionPicker } from "./SessionMentionPicker" import { formatRelativeDate } from "../../utils/date" import { WorktreeMentionPicker } from "./WorktreeMentionPicker" @@ -277,6 +278,7 @@ export const PromptInput: Component = (props) => { let highlightRef: HTMLDivElement | undefined let dropdownRef: HTMLDivElement | undefined let slashDropdownRef: HTMLDivElement | undefined + let containerRef: HTMLDivElement | undefined /** * True after the last menu entry of a bare `@`, which lists the entries above @@ -1637,10 +1639,23 @@ export const PromptInput: Component = (props) => { if (textareaRef) textareaRef.style.height = "auto" } + onMount(() => { + registerPromptMentionDrop(containerRef, (drop) => { + const ref = textareaRef + if (!ref || !ref.isConnected || readonly()) return false + return mention.insertDrop(drop, ref, setText, server.workspaceDirectory(), adjustHeight) + }) + onCleanup(() => registerPromptMentionDrop(undefined, undefined)) + }) + return (
{ diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabStrip.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabStrip.tsx index 51530fd06801..6ec15453701e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabStrip.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabStrip.tsx @@ -13,7 +13,8 @@ import { useVSCode } from "../../context/vscode" import { SessionTab } from "./SessionTab" import { SessionTabMenu } from "./SessionTabMenu" import { SessionTabSwitcher } from "./SessionTabSwitcher" -import { ConstrainDragYAxis, SortableTabContainer } from "./TabDnd" +import { ConstrainDragYAxis, SortableTabContainer, outsideTabBar } from "./TabDnd" +import { beginPromptMentionDrop, endPromptMentionDrop, sessionDrop } from "../../utils/prompt-mention-drop" export const SessionTabStrip: Component = () => { const tabs = useLocalTabs() @@ -86,13 +87,20 @@ export const SessionTabStrip: Component = () => { if (typeof id !== "string") return freeze() setDragging(id) + if (isPendingTab(id)) return + const item = items().get(id) + beginPromptMentionDrop(sessionDrop(item ?? { id })) } const dragOver = (event: DragEvent) => { + // Once the tab is below the bar it is on its way to the prompt, so stop + // reordering the tabs under it. + if (outsideTabBar(event)) return const from = event.draggable?.id const to = event.droppable?.id if (typeof from === "string" && typeof to === "string") tabs.reorder(from, to) } const dragEnd = () => { + endPromptMentionDrop() setDragging(undefined) release() tabs.persist() diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx index bd86ada3d5ad..291646639422 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx @@ -6,14 +6,37 @@ declare module "solid-js" { } } -import { createSortable, useDragDropContext, type Transformer } from "@thisbeyond/solid-dnd" +import { createSortable, useDragDropContext, type Transformer, type DragEvent } from "@thisbeyond/solid-dnd" import { createRoot, onCleanup, type Component, type ParentComponent } from "solid-js" +import { promptMentionDragging } from "../../utils/prompt-mention-drop" +/** + * True once a dragged tab has moved below the tab bar, which means it left the + * bar on the way to the prompt. Reorder must stop at that point so the tabs do + * not keep animating under the pointer. + */ +export function outsideTabBar(event: DragEvent): boolean { + return event.draggable.transformed.center.y > event.draggable.layout.bottom +} + +/** Same idea for a side panel tab strip, where leaving means moving left. */ +export function outsideSidePanel(event: DragEvent): boolean { + return event.draggable.transformed.center.x < event.draggable.layout.left +} + +/** + * Keep tab drags in the tab bar normally, but allow a session tab to move down + * out of the bar while it is being dragged to the prompt. + */ export const ConstrainDragYAxis: Component = () => { const context = useDragDropContext() if (!context) return null const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context - const transformer: Transformer = { id: "constrain-y-axis", order: 100, callback: (value) => ({ ...value, y: 0 }) } + const transformer: Transformer = { + id: "constrain-y-axis", + order: 100, + callback: (value) => ({ ...value, y: promptMentionDragging() ? Math.max(0, value.y) : 0 }), + } const dispose = createRoot((cleanup) => { onDragStart(({ draggable }) => { if (draggable) addTransformer("draggables", draggable.id as string, transformer) diff --git a/packages/kilo-vscode/webview-ui/src/hooks/file-mention-utils.ts b/packages/kilo-vscode/webview-ui/src/hooks/file-mention-utils.ts index ff7842eb9916..ca0ac161762b 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/file-mention-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/file-mention-utils.ts @@ -24,6 +24,16 @@ export type WorktreeReference = { disabled: boolean } +/** + * A mention inserted directly at the caret without an open `@` query, for + * example when a session tab or worktree card is dropped on the prompt. + */ +export type PromptMentionDrop = + | { kind: "worktree"; worktree: WorktreeReference } + | { kind: "session"; session: SessionSearchItem } + | { kind: "terminal" } + | { kind: "file"; path: string } + export const PAST_CHATS_MENTION = "past-chats" const model = { diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts b/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts index 370f0e843e9c..b7631b77f086 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts @@ -28,8 +28,11 @@ import { syncMentionedSessions as _syncMentionedSessions, FILE_PICKER_RESULT, type MentionResult, + type PromptMentionDrop, type WorktreeReference, } from "./file-mention-utils" +import { TERMINAL_MENTION } from "./terminal-context-utils" +import { convertToMentionPath } from "../utils/path-mentions" const FILE_SEARCH_DEBOUNCE_MS = 150 /** Past chats offered to the ranking, bounded so chats cannot flood the list. */ @@ -145,6 +148,14 @@ export interface FileMention { ) => void /** Insert a model reference picked from the model picker as an @-mention. */ selectModelReference: (providerID: string, modelID: string, onSelect?: () => void) => void + /** Insert a dragged reference at the caret (no open @ query). Returns true when inserted. */ + insertDrop: ( + drop: PromptMentionDrop, + textarea: HTMLTextAreaElement, + setText: (text: string) => void, + cwd: string, + onSelect?: () => void, + ) => boolean } export function useFileMention( @@ -555,6 +566,20 @@ export function useFileMention( } } + // Replace a textarea range through execCommand so the change lands on the + // browser's native undo stack. Restore focus first: pickers and drags can + // leave the textarea unfocused, which makes execCommand silently no-op. + const replaceRange = (textarea: HTMLTextAreaElement, start: number, end: number, value: string) => { + textarea.focus() + suppress = true + try { + textarea.setSelectionRange(start, end) + document.execCommand("insertText", false, value) + } finally { + suppress = false + } + } + const selectMention = ( result: MentionResult, textarea: HTMLTextAreaElement, @@ -621,17 +646,7 @@ export function useFileMention( const atPos = match.index! + prefix const suffix = /^\s/.test(after) ? "" : " " remember(atPos, token) - // Restore focus before execCommand: pickers (session search, native file - // dialog) move focus away from the textarea, which makes execCommand - // silently no-op. - textarea.focus() - suppress = true - try { - textarea.setSelectionRange(atPos, cursor) - document.execCommand("insertText", false, `@${token}${suffix}`) - } finally { - suppress = false - } + replaceRange(textarea, atPos, cursor, `@${token}${suffix}`) textarea.focus() @@ -698,6 +713,59 @@ export function useFileMention( // When true, onInput skips dropdown logic (used during execCommand changes) let suppress = false + const insertToken = ( + token: string, + textarea: HTMLTextAreaElement, + setText: (text: string) => void, + onSelect?: () => void, + ): boolean => { + const val = textarea.value + const start = textarea.selectionStart ?? val.length + const end = textarea.selectionEnd ?? start + const before = val.substring(0, start) + const after = val.substring(end) + const prefix = before.length > 0 && !/\s$/.test(before) ? " " : "" + // Always leave a trailing space so the user can keep typing after a drop. + const suffix = /^\s/.test(after) ? "" : " " + replaceRange(textarea, start, end, `${prefix}@${token}${suffix}`) + // The browser fires an input event for execCommand, but tests and some edge + // paths do not, so sync from the textarea to register the mention. + syncMentionedPaths(textarea.value) + setText(textarea.value) + onSelect?.() + return true + } + + const insertDrop = ( + drop: PromptMentionDrop, + textarea: HTMLTextAreaElement, + setText: (text: string) => void, + cwd: string, + onSelect?: () => void, + ): boolean => { + if (drop.kind === "worktree") { + if (drop.worktree.disabled) return false + // Register before execCommand so the input sync finds the path and it is + // not turned into a plain file attachment. + knownWorktrees.set(drop.worktree.path, drop.worktree) + knownPaths.add(drop.worktree.path) + return insertToken(drop.worktree.path, textarea, setText, onSelect) + } + if (drop.kind === "session") { + const normalized = { ...drop.session, title: sessionMentionText(drop.session.title) } + const token = sessionMentionToken(normalized, knownSessions) + // Register before execCommand so the input sync finds the token. + knownSessions.set(token, normalized) + return insertToken(token, textarea, setText, onSelect) + } + if (drop.kind === "terminal") return insertToken(TERMINAL_MENTION, textarea, setText, onSelect) + const resolved = convertToMentionPath(drop.path, cwd) + if (cwd) workspaceDir = cwd + // Register before execCommand so the input sync finds the path. + knownPaths.add(resolved) + return insertToken(resolved, textarea, setText, onSelect) + } + const onInput = (val: string, cursor: number) => { syncScope() syncMentionedPaths(val) @@ -1038,5 +1106,6 @@ export function useFileMention( seedSessions, selectSession, selectModelReference, + insertDrop, } } diff --git a/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css b/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css index 0dea4878a0df..be5573ee8e8f 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css +++ b/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css @@ -349,6 +349,10 @@ border-style: dashed; } +.prompt-input-container--mention-drop { + border-color: var(--border-focus, var(--vscode-focusBorder, #007fd4)); +} + .prompt-input-wrapper { display: flex; gap: 8px; diff --git a/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts b/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts new file mode 100644 index 000000000000..81d54db7c11e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts @@ -0,0 +1,83 @@ +import { createSignal, type Accessor } from "solid-js" +import type { PromptMentionDrop } from "../hooks/file-mention-utils" + +export type { PromptMentionDrop } + +/** Drag payload for a session tab or card. Normalizes the title and timestamp. */ +export function sessionDrop(session: { id: string; title?: string; updatedAt?: string }): PromptMentionDrop { + return { + kind: "session", + session: { + id: session.id, + title: session.title?.trim() || session.id, + updated: Date.parse(session.updatedAt ?? "") || Date.now(), + }, + } +} + +type Target = { + element: HTMLElement + insert: (drop: PromptMentionDrop) => boolean +} + +type Point = { x: number; y: number } + +type Rect = { left: number; top: number; right: number; bottom: number } + +let target: Target | undefined +let active: PromptMentionDrop | undefined +let point: Point | undefined + +const [over, setOver] = createSignal(false) +const [dragging, setDragging] = createSignal(false) + +export function insideRect(rect: Rect, x: number, y: number): boolean { + return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom +} + +const inside = (x: number, y: number) => { + const element = target?.element + if (!element || !element.isConnected) return false + const rect = element.getBoundingClientRect() + return insideRect({ left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }, x, y) +} + +const move = (event: PointerEvent) => { + point = { x: event.clientX, y: event.clientY } + setOver(inside(event.clientX, event.clientY)) +} + +export function registerPromptMentionDrop( + element: HTMLElement | undefined, + insert: ((drop: PromptMentionDrop) => boolean) | undefined, +) { + target = element && insert ? { element, insert } : undefined + if (!element) setOver(false) +} + +export function beginPromptMentionDrop(drop: PromptMentionDrop) { + if (!target) return + active = drop + point = undefined + setOver(false) + setDragging(true) + document.addEventListener("pointermove", move) +} + +/** Resolve the drop. Returns true when the prompt inserted the mention. */ +export function endPromptMentionDrop(): boolean { + const drop = active + if (!drop) return false + document.removeEventListener("pointermove", move) + const hit = point !== undefined && inside(point.x, point.y) + const insert = hit ? target?.insert : undefined + active = undefined + point = undefined + setOver(false) + setDragging(false) + return insert?.(drop) ?? false +} + +export const promptMentionOver: Accessor = over +/** True between a prompt mention drag start and its drag end. */ +export const promptMentionDragging: Accessor = dragging From ff5aeba07bee57b81c4c0615497c485aabbea110 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 14 Sep 2026 09:38:07 +0200 Subject: [PATCH 2/3] fix(agent-manager): address drag-to-prompt review feedback - Track dropped mentions so typing after a drop keeps the dropdown closed - Detect leaving the side panel via strip bounds so leftward reordering works - Tear down the drop listener and drag state when the prompt unmounts - Restore sidebar worktree order on prompt and outside drops - Derive the disabled flag for drag-dropped worktree references --- .../tests/unit/prompt-mention-drop.test.ts | 17 +++++++++++++++++ .../tests/unit/use-file-mention.test.ts | 8 ++++++++ .../agent-manager/InspectorTabStrip.tsx | 7 ++++--- .../agent-manager/ProjectSidebarBody.tsx | 8 ++++---- .../webview-ui/agent-manager/SidebarBody.tsx | 16 +++++++++++++--- .../agent-manager/worktree-references.ts | 3 ++- .../webview-ui/src/components/chat/TabDnd.tsx | 5 ----- .../webview-ui/src/hooks/useFileMention.ts | 4 ++++ .../webview-ui/src/utils/prompt-mention-drop.ts | 12 +++++++++++- 9 files changed, 63 insertions(+), 17 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts b/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts index c6fbed3a1642..4c0cf43efdc5 100644 --- a/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-mention-drop.test.ts @@ -3,6 +3,8 @@ import { beginPromptMentionDrop, endPromptMentionDrop, insideRect, + promptMentionDragging, + promptMentionOver, registerPromptMentionDrop, type PromptMentionDrop, } from "../../webview-ui/src/utils/prompt-mention-drop" @@ -97,4 +99,19 @@ describe("prompt mention drop", () => { expect(listeners.size).toBe(0) expect(endPromptMentionDrop()).toBe(false) }) + + it("tears down an active drag when the prompt unmounts", () => { + mockDocument() + registerPromptMentionDrop(target(), () => true) + beginPromptMentionDrop(drop) + expect(listeners.size).toBe(1) + expect(promptMentionDragging()).toBe(true) + + registerPromptMentionDrop(undefined, undefined) + + expect(listeners.size).toBe(0) + expect(promptMentionDragging()).toBe(false) + expect(promptMentionOver()).toBe(false) + expect(endPromptMentionDrop()).toBe(false) + }) }) diff --git a/packages/kilo-vscode/tests/unit/use-file-mention.test.ts b/packages/kilo-vscode/tests/unit/use-file-mention.test.ts index 24f3232c41d5..1993c102eb89 100644 --- a/packages/kilo-vscode/tests/unit/use-file-mention.test.ts +++ b/packages/kilo-vscode/tests/unit/use-file-mention.test.ts @@ -1998,6 +1998,14 @@ describe("useFileMention reference drops", () => { }) }) + it("keeps the dropdown closed while typing after a dropped mention", () => { + withMention("", undefined, (mention, area) => { + mention.insertDrop({ kind: "terminal" }, area, () => {}, "") + mention.onInput("@terminal what failed", 21) + expect(mention.showMention()).toBe(false) + }) + }) + it("inserts a relative file reference from a document tab", () => { withMention("", undefined, (mention, area) => { expect(mention.insertDrop({ kind: "file", path: "/repo/docs/plan.md" }, area, () => {}, "/repo")).toBe(true) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx b/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx index 483651f19461..d52065b956b0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/InspectorTabStrip.tsx @@ -7,7 +7,7 @@ import { type DragEvent, } from "@thisbeyond/solid-dnd" import { For, Show, createSignal, type Accessor, type Component, type JSX } from "solid-js" -import { ConstrainDragYAxis, outsideSidePanel } from "../src/components/chat/TabDnd" +import { ConstrainDragYAxis } from "../src/components/chat/TabDnd" import { beginPromptMentionDrop, endPromptMentionDrop, type PromptMentionDrop } from "../src/utils/prompt-mention-drop" import { createTabFocus } from "../src/utils/tab-navigation" import { useTabScroll } from "../src/utils/tab-scroll" @@ -60,8 +60,9 @@ export const InspectorTabStrip: Component = (props) => { } const over = (event: DragEvent) => { // Once the tab leaves the side panel it is on its way to the prompt, so stop - // reordering the tabs under it. Only applies to drag-to-prompt strips. - if (props.drag && outsideSidePanel(event)) return + // reordering the tabs under it. Use the strip bounds, not the tab's own + // rect, so leftward reordering inside the strip still works. + if (props.drag && event.draggable.transformed.center.x < host.getBoundingClientRect().left) return const from = event.draggable?.id const to = event.droppable?.id if (typeof from !== "string" || typeof to !== "string") return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index ae99be9ed652..e11abc47e9c2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -211,6 +211,7 @@ export const ProjectSidebarBody: Component = (props) => { wt, wt.label || firstOrderedTitle(sessions(wt.id), store.tabOrder()[wt.id], wt.branch), sessions(wt.id).map((session) => ({ id: session.id })), + wt.id === props.selection || store.staleWorktreeIds().has(wt.id) || store.busy().has(wt.id), ), }) } @@ -243,10 +244,9 @@ export const ProjectSidebarBody: Component = (props) => { setDragOrigin(undefined) document.body.classList.remove("am-wt-dragging-active") // A drop on the prompt inserts a mention. Do not also move the worktree to - // whatever section happens to be under the pointer. - if (handled) return - // A release outside the sidebar is not a section move or list reorder. - if (outsideSidebar(event.draggable)) { + // whatever section happens to be under the pointer. Both this path and an + // outside release undo the reorder applied while passing over sibling rows. + if (handled || outsideSidebar(event.draggable)) { if (origin) store.setWorktreeOrder(origin) return } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx index d1cb1ef22c5b..100add44a6dd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx @@ -106,6 +106,9 @@ export const SidebarBody: Component = (props) => { const vscode = useVSCode() const updateBase = useBaseUpdate() const localState = () => props.activityFor(null) + // Captured at worktree drag start so a release outside the sidebar, or a drop + // on the prompt, can undo a reorder applied while passing over sibling rows. + let origin: string[] | undefined return ( <> @@ -254,6 +257,7 @@ export const SidebarBody: Component = (props) => { const id = event.draggable?.id if (typeof id === "string") { props.setDraggingWorktree(id) + origin = props.sidebarWorktreeOrder() const wt = sorted().find((item) => item.id === id) if (wt) { beginPromptMentionDrop({ @@ -265,6 +269,7 @@ export const SidebarBody: Component = (props) => { .managedSessions() .filter((session) => session.worktreeId === wt.id) .map((session) => ({ id: session.id })), + wt.id === props.selection() || props.isStaleWorktree(wt.id) || props.busy(wt.id), ), }) } @@ -295,9 +300,14 @@ export const SidebarBody: Component = (props) => { document.body.classList.remove("am-wt-dragging-active") // A drop on the prompt inserts a mention. Do not also move the // worktree to whatever section happens to be under the pointer. - if (handled) return - // A release outside the sidebar is not a list reorder. - if (outsideSidebar(event.draggable)) return + // Both this path and an outside release undo the pass-over + // reorder so the sidebar matches the persisted order. + if (handled || outsideSidebar(event.draggable)) { + if (origin) props.setSidebarWorktreeOrder(() => origin!) + origin = undefined + return + } + origin = undefined if (typeof from === "string" && typeof to === "string" && secIds().has(to)) { props.moveToSection([from], to) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts b/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts index 3dc30019e84a..dc4bd23ccef6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/worktree-references.ts @@ -60,6 +60,7 @@ export function worktreeDropReference( worktree: WorktreeState, name: string, sessions: { id: string; title?: string }[], + disabled: boolean, ): WorktreeReference { return { id: worktree.id, @@ -68,7 +69,7 @@ export function worktreeDropReference( path: worktree.path, base: worktree.parentBranch, sessions, - disabled: false, + disabled, } } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx index 291646639422..24921b6e225c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx @@ -19,11 +19,6 @@ export function outsideTabBar(event: DragEvent): boolean { return event.draggable.transformed.center.y > event.draggable.layout.bottom } -/** Same idea for a side panel tab strip, where leaving means moving left. */ -export function outsideSidePanel(event: DragEvent): boolean { - return event.draggable.transformed.center.x < event.draggable.layout.left -} - /** * Keep tab drags in the tab bar normally, but allow a session tab to move down * out of the bar while it is being dragged to the prompt. diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts b/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts index b7631b77f086..4b938070518d 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts @@ -727,11 +727,15 @@ export function useFileMention( const prefix = before.length > 0 && !/\s$/.test(before) ? " " : "" // Always leave a trailing space so the user can keep typing after a drop. const suffix = /^\s/.test(after) ? "" : " " + // Record the inserted mention so onInput reads text typed after it as prose + // instead of reopening the @ dropdown for "@token prose". + remember(start + prefix.length, token) replaceRange(textarea, start, end, `${prefix}@${token}${suffix}`) // The browser fires an input event for execCommand, but tests and some edge // paths do not, so sync from the textarea to register the mention. syncMentionedPaths(textarea.value) setText(textarea.value) + closeMention() onSelect?.() return true } diff --git a/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts b/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts index 81d54db7c11e..a78c631df9d6 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/prompt-mention-drop.ts @@ -52,7 +52,17 @@ export function registerPromptMentionDrop( insert: ((drop: PromptMentionDrop) => boolean) | undefined, ) { target = element && insert ? { element, insert } : undefined - if (!element) setOver(false) + if (target) return + // Unmounting mid-drag must not leak the pointer listener or the active drop. + // Otherwise ordinary pointer movement flips the highlight and keeps the tab + // constraint loose until an unrelated drag happens to end. + if (active) { + document.removeEventListener("pointermove", move) + active = undefined + point = undefined + setDragging(false) + } + setOver(false) } export function beginPromptMentionDrop(drop: PromptMentionDrop) { From 79663af7dd4d9530efb8e7226e902fa2e79a025c Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Mon, 14 Sep 2026 07:43:42 +0000 Subject: [PATCH 3/3] chore: update kilo-vscode visual regression baselines --- .../full-screen-diff-with-changes-chromium-linux.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-changes-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-changes-chromium-linux.png index af3bd72e5748..ce2efcbd870c 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-changes-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-changes-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:003ea4d8d6412bbda7ad30c2658baf6d69f75232a0494e3d07f28793e4602f5e -size 53514 +oid sha256:ffcf0db51ace530056ba92e57295670ce51f311a8428586cfec51520691a002c +size 53216