feat: sidebar_open tool gates model-driven sidebar opens behind a global setting
New global Side card setting `agentOpenTools` (default off, 常规 group): when enabled, one `sidebar_open` tool is injected into the model so it can actively open a local file, a local folder (tree rooted there), or an HTTP(S) page in the calling session's sidebar. - host: AgentOpenRegistry + registerOpenTool (src/agent-opens.ts); the tool classifies http(s) URLs / stat-driven file/folder paths, refuses other schemes, resolves relative paths against the session cwd, and reports target-tab-disabled instead of silently no-oping. Gated like agentTerminalTools (register/drain on settings watch). - delivery: /sidebar/ws/agent-opens push (consume-on-send queue, replay on attach); the tool result reports `delivered` so queued opens for non-active sessions are honest. - client: Sidebar subscribes and routes pushes to editor (file), folder window (meta.dir, tree rooted at the folder — EditorHost), or browser (URL) tabs; settings row in 常规 + 20 locale dictionaries. - tests: agent-opens registry/tool spec, smoke gating + WS route list, prefs/plugin-shape defaults, editor-host folder window, side-card rows. - docs: design doc, AGENTS.md key lists + reference, README feature bullets.
This commit is contained in:
@@ -103,6 +103,11 @@ export function EditorHost(props: {
|
||||
const { ctx, store, scope, tab, expanded, onToggleDir, onReferenceFile } = props
|
||||
const path = tab.path ?? ''
|
||||
const title = tab.title
|
||||
// A folder window: the model's `sidebar_open` (or any caller) opens a
|
||||
// directory as an editor tab carrying `meta.dir: true` with the directory
|
||||
// as its path. It renders the file tree rooted at that folder instead of
|
||||
// the viewer loading flow (a directory is not a file).
|
||||
const isDir = metaOf(tab).dir === true
|
||||
const [load, setLoad] = useState<EditorLoad>({ status: 'loading' })
|
||||
|
||||
// Reactive prefs read: flipping editorExplorer re-renders this tab with no
|
||||
@@ -123,9 +128,12 @@ export function EditorHost(props: {
|
||||
const openWithConfig = useMemo(() => parseOpenWithConfig(editorBlob.openWith), [editorBlob])
|
||||
const openWithTargets = useMemo(() => resolveOpenWithTargets(openWithConfig), [openWithConfig])
|
||||
// A path-less tab shows the empty-state hint in merged mode — and in split
|
||||
// mode it is the standalone explorer (tree-only, see the render below).
|
||||
// mode it is the standalone explorer (tree-only, see the render below). A
|
||||
// folder tab is a folder window in BOTH modes: the tree rooted at the
|
||||
// folder, no editor chrome.
|
||||
const showEmpty = path === ''
|
||||
const treeOnly = showEmpty && !inPlace
|
||||
const folderRoot = isDir ? path : undefined
|
||||
|
||||
/**
|
||||
* Open a file from THIS window (tree click / search row / path input):
|
||||
@@ -258,8 +266,9 @@ export function EditorHost(props: {
|
||||
// fresh viewer re-registers its own.
|
||||
setToolbar(null)
|
||||
// The seeded home tab (no path) never loads a viewer — the empty-state
|
||||
// hint renders until the user picks a file.
|
||||
if (showEmpty) return
|
||||
// hint renders until the user picks a file. A folder tab never loads a
|
||||
// viewer either — its tree is rooted at the folder.
|
||||
if (showEmpty || isDir) return
|
||||
let cancelled = false
|
||||
// Aborts the matched viewer's `load` when the editor tears down (tab
|
||||
// closed, path changed, session switched) or re-matches the viewer.
|
||||
@@ -311,7 +320,7 @@ export function EditorHost(props: {
|
||||
}
|
||||
apply(planFirstMatch(ctx.betterSidebar?.matchFileViewer(path), mediaUrlOf))
|
||||
return () => { cancelled = true; controller.abort() }
|
||||
}, [scope.sessionId, scope.cwd, path, ctx, showEmpty])
|
||||
}, [scope.sessionId, scope.cwd, path, ctx, showEmpty, isDir])
|
||||
|
||||
const treeOpen = treeOpenOf(tab)
|
||||
/** Persist the panel flag on the tab (survives reloads with the layout). */
|
||||
@@ -324,13 +333,15 @@ export function EditorHost(props: {
|
||||
// Split mode: the path-less window IS the standalone explorer — the tree
|
||||
// panel fills the whole tab (search + FileTree, full form), no editor
|
||||
// chrome. File opens land in new per-path tabs through openFile above.
|
||||
if (treeOnly) {
|
||||
// A folder window (meta.dir, any mode) renders the SAME surface rooted
|
||||
// at the folder instead of the session cwd.
|
||||
if (treeOnly || folderRoot !== undefined) {
|
||||
return (
|
||||
<div className={css.editor}>
|
||||
<TreePanel
|
||||
full
|
||||
sessionId={scope.sessionId}
|
||||
cwd={scope.cwd}
|
||||
cwd={folderRoot ?? scope.cwd}
|
||||
expanded={expanded}
|
||||
onToggle={onToggleDir}
|
||||
onOpenFile={openFile}
|
||||
|
||||
Reference in New Issue
Block a user