feat(composer): inline @file and /skill tags, commands anywhere, project drag-and-drop (#2066)

Closes #1981

@file references and /skill commands become inline tags anywhere in the prompt, in Home and in a conversation. Tags are edited as a unit (Backspace/Delete, arrows) and are not committed mid-IME composition. Files and folders dragged from the Project view become references, mapped into the task worktree; paths outside the project, traversal and relative paths are rejected, and a deleted reference can be dropped again.

Co-authored-by: panda-student <[email protected]>
Цей коміт міститься в:
SunZhiQiang authored and GitHub committed 2026-11-08 05:54:24 +08:00
1 джерело 45a1132ece
коміт ff7473a277
17 files changed
+545 -115

No files matched your search

+12 -5
Переглянути файл
@@ -57,6 +57,11 @@ import { CodeBreadcrumbs } from "./StatusItems";
import styles from "./EditorArea.module.css";
import { tx } from "@/platform/i18n";
/** The documents of a project-files drag: folders (trailing `/`) are not opened as editors. */
function droppedFiles(payload: string): string[] {
return payload.split("\n").filter((p) => !!p && !p.endsWith("/"));
}
export function EditorAreaView() {
const layout = useTabs((s) => s.layout);
const empty = useTabs((s) => Object.values(s.panes).every((p) => p.tabs.length === 0));
@@ -77,14 +82,14 @@ function EmptyEditorArea() {
if (!el) return;
return registerDropTarget(el, {
accepts: (type) => type === EDITOR_TAB_MIME || type === FILES_DRAG || acceptsEditorDrop(type),
over: () => setOver(true),
over: (data) => setOver(data.type !== FILES_DRAG || droppedFiles(data.payload).length > 0),
leave: () => setOver(false),
drop: (data) => {
setOver(false);
const paneId = Object.keys(useTabs.getState().panes)[0];
if (!paneId) return;
if (data.type === FILES_DRAG) {
void editorController().openFilesIn(data.payload.split("\n").filter(Boolean), paneId, "center");
void editorController().openFilesIn(droppedFiles(data.payload), paneId, "center");
return;
}
if (data.type !== EDITOR_TAB_MIME) {
@@ -253,14 +258,16 @@ function PaneView({ paneId }: { paneId: string }) {
const zoneAt = (x: number, y: number) => dropZoneAt(x, y, el.getBoundingClientRect());
return registerDropTarget(el, {
accepts: (type) => type === EDITOR_TAB_MIME || type === FILES_DRAG || acceptsEditorDrop(type),
over: (data, x, y) =>
setDropZone(data.type === EDITOR_TAB_MIME || data.type === FILES_DRAG ? zoneAt(x, y) : "center"),
over: (data, x, y) => {
if (data.type === FILES_DRAG) setDropZone(droppedFiles(data.payload).length ? zoneAt(x, y) : null);
else setDropZone(data.type === EDITOR_TAB_MIME ? zoneAt(x, y) : "center");
},
leave: () => setDropZone(null),
drop: (data, x, y) => {
const zone = zoneAt(x, y);
setDropZone(null);
if (data.type === FILES_DRAG) {
void ctrl.openFilesIn(data.payload.split("\n").filter(Boolean), paneId, zone);
void ctrl.openFilesIn(droppedFiles(data.payload), paneId, zone);
return;
}
if (data.type !== EDITOR_TAB_MIME) {