дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(agents): dock a session in the Agents tool window (#588)
Agent sessions can be opened in the Agents tool window beside the editor (Open in Tool Window / Open in Editor in the task menus, or by dragging between the editor and the tool window); opening in an editor tab stays the default. Fixes in review: - A docked conversation kept the previous task's subagent view after switching tasks: the session's local state is now keyed by task. - Moving a conversation between the editor and the tool window lost its scroll position: position and follow-output are kept per conversation (task or subagent). - In a short bottom tool window the message area shrank to a few pixels: wide windows place the task list beside the conversation, narrow ones cap the list height. - Drag and drop and list selection now go through the agents.dockTask / undockTask / openTask actions. Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
1 джерело
fa26ba842c
коміт
9847fd2c01
18 files changed
+982
-103
No files matched your search
@@ -1458,6 +1458,11 @@ export class EditorController implements SlotHost {
|
||||
return [...out];
|
||||
}
|
||||
|
||||
/** Input of the tab `tabId` in pane `paneId` (dragging a tab between views), null if there is none. */
|
||||
tabInput(paneId: string, tabId: string): EditorInput | null {
|
||||
return useTabs.getState().panes[paneId]?.tabs.find((t) => t.id === tabId)?.input ?? null;
|
||||
}
|
||||
|
||||
closePath(path: string): Promise<boolean> {
|
||||
return this.removeGuarded((s) => {
|
||||
const keys = new Set<string>();
|
||||
@@ -1682,7 +1687,10 @@ export class EditorController implements SlotHost {
|
||||
{ kind: "action", action: "project.rename", arg: path, label: "Rename File…" },
|
||||
{ kind: "separator" },
|
||||
] as MenuItem[])
|
||||
: []),
|
||||
: [
|
||||
// Contributed groups for tabs without a file (e.g. "Open in Tool Window").
|
||||
...menuGroupItems("editorTab", { paths: [], input: tab.input }),
|
||||
]),
|
||||
{ kind: "action", action: "file.settings", arg: "editor.general.editorTabs", label: "Configure Editor Tabs…" },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function activate(): void | (() => void) {
|
||||
focus: () => ctrl.focus(),
|
||||
openPaths: () => ctrl.openPaths(),
|
||||
openTabs: () => ctrl.openTabs(),
|
||||
tabInput: (paneId, tabId) => ctrl.tabInput(paneId, tabId),
|
||||
activeInput: () => ctrl.activeInput(),
|
||||
onActiveInputChange: (fn) => ctrl.onActiveInputChange(fn),
|
||||
closePath: (path) => ctrl.closePath(path),
|
||||
|
||||
@@ -13,6 +13,9 @@ import { setContext } from "@/platform/context";
|
||||
import { useEditorPrefs } from "@/platform/editorSettings";
|
||||
import { basename, dirname } from "@/platform/project";
|
||||
import {
|
||||
EDITOR_TAB_MIME,
|
||||
acceptsEditorDrop,
|
||||
dropOnEditor,
|
||||
getFileDecoration,
|
||||
prepareMenuGroups,
|
||||
textEditorWrapperFor,
|
||||
@@ -54,7 +57,7 @@ import { tx } from "@/platform/i18n";
|
||||
export function EditorAreaView() {
|
||||
const layout = useTabs((s) => s.layout);
|
||||
const empty = useTabs((s) => Object.values(s.panes).every((p) => p.tabs.length === 0));
|
||||
if (empty && layout.kind === "pane") return <EmptyEditor />;
|
||||
if (empty && layout.kind === "pane") return <EmptyEditorArea />;
|
||||
return (
|
||||
<div className={styles.area}>
|
||||
<LayoutView node={layout} />
|
||||
@@ -62,6 +65,42 @@ export function EditorAreaView() {
|
||||
);
|
||||
}
|
||||
|
||||
/** The empty editor still accepts drops: reopening tabs, opening files, feature drags (agent tasks). */
|
||||
function EmptyEditorArea() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [over, setOver] = useState(false);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
return registerDropTarget(el, {
|
||||
accepts: (type) => type === EDITOR_TAB_MIME || type === FILES_DRAG || acceptsEditorDrop(type),
|
||||
over: () => setOver(true),
|
||||
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");
|
||||
return;
|
||||
}
|
||||
if (data.type !== EDITOR_TAB_MIME) {
|
||||
dropOnEditor(data);
|
||||
return;
|
||||
}
|
||||
const p = parseTabPayload(data.payload);
|
||||
if (p) editorController().moveTab(p.paneId, p.tabId, paneId, 0);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div ref={ref} className={styles.area}>
|
||||
<EmptyEditor />
|
||||
{over && <div className={clsx(styles.dropZone, styles["drop-center"])} aria-hidden="true" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutView({ node }: { node: LayoutNode }): ReactNode {
|
||||
if (node.kind === "pane") return <PaneView paneId={node.paneId} />;
|
||||
return (
|
||||
@@ -78,9 +117,6 @@ function LayoutView({ node }: { node: LayoutNode }): ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
/** MIME type of a dragged editor tab: `{"paneId","tabId"}` JSON. */
|
||||
export const EDITOR_TAB_MIME = "application/x-openidea-editor-tab";
|
||||
|
||||
type DropZone = SplitEdge | "center";
|
||||
|
||||
/** Where a tab dropped at (x, y) over an editor goes: an edge (new split) or the pane itself. */
|
||||
@@ -232,8 +268,9 @@ function PaneView({ paneId }: { paneId: string }) {
|
||||
if (!el) return;
|
||||
const zoneAt = (x: number, y: number) => dropZoneAt(x, y, el.getBoundingClientRect());
|
||||
return registerDropTarget(el, {
|
||||
accepts: (type) => type === EDITOR_TAB_MIME || type === FILES_DRAG,
|
||||
over: (_, x, y) => setDropZone(zoneAt(x, y)),
|
||||
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"),
|
||||
leave: () => setDropZone(null),
|
||||
drop: (data, x, y) => {
|
||||
const zone = zoneAt(x, y);
|
||||
@@ -242,6 +279,10 @@ function PaneView({ paneId }: { paneId: string }) {
|
||||
void ctrl.openFilesIn(data.payload.split("\n").filter(Boolean), paneId, zone);
|
||||
return;
|
||||
}
|
||||
if (data.type !== EDITOR_TAB_MIME) {
|
||||
dropOnEditor(data);
|
||||
return;
|
||||
}
|
||||
const p = parseTabPayload(data.payload);
|
||||
if (!p) return;
|
||||
const target = useTabs.getState().panes[paneId];
|
||||
|
||||
@@ -28,6 +28,9 @@ const { createMemoryBackend } = await import("./backend");
|
||||
const { initialTabsState } = await import("./model/tabs");
|
||||
const { serializeSession } = await import("./model/session");
|
||||
const { useDocMeta } = await import("./model/document");
|
||||
const { registerMenuGroup } = await import("@/workbench/contributions");
|
||||
const { registerActions } = await import("@/platform/actions");
|
||||
const { resolveMenuItems } = await import("@/ui");
|
||||
|
||||
let ctrl: InstanceType<typeof EditorController> | null = null;
|
||||
|
||||
@@ -85,6 +88,25 @@ describe("virtual editor input", () => {
|
||||
.tabMenu(useTabs.getState().activePane, activeTab())
|
||||
.some((i) => "label" in i && i.label === "Reveal in Project"),
|
||||
).toBe(false);
|
||||
|
||||
// Pathless tabs still get contributed groups, with the tab's input in the context.
|
||||
const disposeAction = registerActions([{ id: "test.tab", title: "Test Item", run: () => undefined }]);
|
||||
const disposeGroup = registerMenuGroup({
|
||||
id: "test.tab",
|
||||
menus: ["editorTab"],
|
||||
order: 50,
|
||||
items: (_menu, { paths, input }) =>
|
||||
paths.length || input?.kind !== "virtual"
|
||||
? []
|
||||
: [{ kind: "action", action: "test.tab", label: "Test Item", arg: input.key }],
|
||||
});
|
||||
try {
|
||||
const items = resolveMenuItems(ctrl.tabMenu(useTabs.getState().activePane, activeTab()));
|
||||
expect(items.some((i) => "label" in i && i.label === "Test Item")).toBe(true);
|
||||
} finally {
|
||||
disposeGroup();
|
||||
disposeAction();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not open a tab when the server has no contents", async () => {
|
||||
|
||||
Reference in new issue
Заблокувати користувача