From 44b2d11b2dce27c59b26f9e3ff2a56cd151ed27f Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:36:55 +0800 Subject: [PATCH] feat(agents): Select the active session in the Agents list (#140) Switching between agent session tabs in the editor now moves the Agents tool window's selection to that task, expanding its group (e.g. Archived) when it is collapsed. The list also opens with the active session selected. Tabs of other kinds leave the selection as it is. The editor service gains activeInput() and onActiveInputChange() so features can follow the active tab of any kind. --- src/editor/core/controller.ts | 14 ++++++++++++++ src/editor/core/index.ts | 2 ++ src/features/agents/AgentsView.tsx | 26 +++++++++++++++++++++++--- src/workbench/contributions.ts | 4 ++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/editor/core/controller.ts b/src/editor/core/controller.ts index 3d66df09..552e5f26 100644 --- a/src/editor/core/controller.ts +++ b/src/editor/core/controller.ts @@ -1093,6 +1093,20 @@ export class EditorController implements SlotHost { this.apply((s) => closeKey(s, key)); } + activeInput(): EditorInput | null { + return this.activeTabOf()?.input ?? null; + } + + onActiveInputChange(fn: (input: EditorInput | null) => void): () => void { + let prev = this.activeInput()?.key; + return useTabs.subscribe(() => { + const input = this.activeInput(); + if (input?.key === prev) return; + prev = input?.key; + fn(input); + }); + } + activePath(): string | null { return this.activeSlot?.model.path ?? this.activeTabOf()?.input.path ?? null; } diff --git a/src/editor/core/index.ts b/src/editor/core/index.ts index c4e95281..e5bd81e4 100644 --- a/src/editor/core/index.ts +++ b/src/editor/core/index.ts @@ -43,6 +43,8 @@ export function activate(): void | (() => void) { focus: () => ctrl.focus(), openPaths: () => ctrl.openPaths(), openTabs: () => ctrl.openTabs(), + activeInput: () => ctrl.activeInput(), + onActiveInputChange: (fn) => ctrl.onActiveInputChange(fn), closePath: (path) => ctrl.closePath(path), closeInput: (key) => ctrl.closeInput(key), reformat: async (path) => { diff --git a/src/features/agents/AgentsView.tsx b/src/features/agents/AgentsView.tsx index d93d078a..e9283606 100644 --- a/src/features/agents/AgentsView.tsx +++ b/src/features/agents/AgentsView.tsx @@ -1,10 +1,11 @@ /** Agents tool window: tasks grouped by what they need from the user. */ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import type { AgentStatus as Status, AgentTask } from "@/bindings"; import { executeAction } from "@/platform/actions"; +import { maybeEditorService, type EditorInput } from "@/workbench/contributions"; import { confirmChecked } from "@/workbench/messages"; import { EmptyState, Icon, IconButton, Spinner, T, Tree, showContextMenu, type MenuItem } from "@/ui"; -import { age, branchLabel, groupTasks, isBusy, type TaskGroup } from "./model"; +import { SESSION_KIND, age, branchLabel, groupTasks, isBusy, type TaskGroup } from "./model"; import { archiveTask, deleteTask, openHome, openTask, useAgents } from "./store"; import styles from "./Agents.module.css"; import { tx } from "@/platform/i18n"; @@ -17,6 +18,9 @@ type Node = { kind: "group"; group: TaskGroup } | { kind: "task"; task: AgentTas const nodeKey = (n: Node) => (n.kind === "group" ? `g:${n.group.status}` : n.task.id); +const sessionTask = (input: EditorInput | null | undefined) => + input?.kind === SESSION_KIND ? (input.data as { task?: string } | undefined)?.task : undefined; + export function TaskStatusIcon({ status }: { status: Status }) { switch (status) { case "needsInput": @@ -39,7 +43,23 @@ export function AgentsView() { const tasks = useAgents((s) => s.tasks); const providers = useAgents((s) => s.providers); const [collapsed, setCollapsed] = useState>(() => new Set(["g:archived"])); - const [selected, setSelected] = useState>(() => new Set()); + const [selected, setSelected] = useState>(() => { + const task = sessionTask(maybeEditorService()?.activeInput?.()); + return new Set(task ? [task] : []); + }); + // Follow the active session tab. + useEffect( + () => + maybeEditorService()?.onActiveInputChange?.((input) => { + const task = sessionTask(input); + if (!task) return; + setSelected(new Set([task])); + const group = groupTasks(useAgents.getState().tasks.values()).find((g) => g.tasks.some((t) => t.id === task)); + const key = group && `g:${group.status}`; + if (key) setCollapsed((c) => (c.has(key) ? new Set([...c].filter((k) => k !== key)) : c)); + }), + [], + ); const roots = useMemo(() => groupTasks(tasks.values()).map((group) => ({ kind: "group", group })), [tasks]); const expanded = useMemo(() => new Set(roots.map(nodeKey).filter((k) => !collapsed.has(k))), [roots, collapsed]); if (!loaded) diff --git a/src/workbench/contributions.ts b/src/workbench/contributions.ts index 6a5b7fb3..ff8d4e2f 100644 --- a/src/workbench/contributions.ts +++ b/src/workbench/contributions.ts @@ -409,6 +409,10 @@ export interface EditorService { openPaths?(): string[]; /** Every open tab, files or not, most recently active first (Switcher). */ openTabs?(): OpenEditorTab[]; + /** Input of the active tab (any kind), if any. */ + activeInput?(): EditorInput | null; + /** Calls `fn` whenever the active tab changes; returns the unsubscribe. */ + onActiveInputChange?(fn: (input: EditorInput | null) => void): () => void; /** Closes every tab showing `path`. */ closePath?(path: string): void; /** Closes the tab of the input with this `key` (e.g. a merge editor once applied). */