[Feature request] A general per-workspace child slot for the workspace overflow menu (sidebar.workspaces.entry)
#1413
Replies: 1 comment
|
Nice write-up — and the current-state analysis checks out. I verified against the running 0.1.0-rc.6 client bundle ( On the proposal itself: +1 for following the
Also worth noting your own link to #1184 (list-slot ordering) is directly relevant — third-party entries projecting into the overflow menu will hit the same ordering semantics, so good that you're thinking about it from the start. Solid proposal — hope the team picks it up. |
Uh oh!
There was an error while loading. Please reload this page.
Background
I'm writing a "skills manager" plugin for dsh: enable/disable skills across session > workspace > global scopes (the host side uses a rank-50 skill provider to block disabled entries, the client side reuses the
skill.listRPC + a settings scope). The global settings page and the sessionconversation.viewtab both already have existing slots.Only the workspace scope has no suitable entry point. My approach is: add a "Manage skills" option to each workspace's three-dot overflow menu, and clicking it opens that workspace's skills panel. But after going through the client source I found there is no way for a plugin to do this.
Current state (why plugins can't do it)
sidebar.workspacesis asingleslot (packages/client/ui-sidebar/src/client/contract/slots.ts):The entire browse area is rendered exclusively by
ui-workspace'sWorkspaceBrowser.WorkspaceBrowserdeclares only one child slot (packages/client/ui-workspace/src/client/contract/slots.ts):It's only the "directory selection" hole, not a per-workspace row/menu extension point.
The workspace three-dot menu is hardcoded (
ProjectRowIteminpackages/client/ui-workspace/src/client/rows/Rows.tsx):Menutakes a descriptor array (MenuItem[]), not React nodes, and there is no registration point to "append external menu items".Conclusion: to make the workspace overflow menu extensible, currently you can only change the
ui-workspacesource.Proposal: a general
sidebar.workspaces.entry(list slot)Follow dsh's existing mature
conversation.view(tabs) andsettings.section(settings nav) pattern, and add alistslot for workspaces:slots.entries()to project into menu-item descriptors, render them into the workspace three-dot menu, and on selectionrenderSlot(key, { workspacePath }, { only: id })opens a modal rendering only that entry.This way any plugin can contribute workspace menu items, with
id/label/ordersemantics exactly matchingconversation.view/settings.section.Appendix: full implementation patch
1. ui-workspace — slot declaration + overflow-menu rendering
packages/client/ui-workspace/src/client/contract/slots.ts:packages/client/ui-workspace/src/client/index.ts:packages/client/ui-workspace/src/client/rows/Rows.tsx(ProjectRowItem):packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx(key spots):type SessionTreeProps = Pick<...> & { ... orderBy: SessionOrderBy + /** Projected per-workspace menu entries (resolved label + id). */ + entryItems: readonly { id: string; label: string }[] + /** Open a per-workspace entry (id from entryItems) for a workspace path. */ + openWorkspaceEntry: (workspacePath: string, id: string) => void } ... function SessionTree({ ..., t, entryItems, openWorkspaceEntry }: SessionTreeProps) { ... <ProjectRowItem ... + extraMenuItems={entryItems} + onEntrySelect={(id) => { + if (group.cwd !== undefined) openWorkspaceEntry(group.cwd, id) + }} /> ... export function WorkspaceBrowser({ ..., renderSlot, workspaceEntryItems, t }: WorkspaceBrowserProps) { const workspaces = useWorkspaces(state => state.items) + const entryItems = workspaceEntryItems() ... + const [entryTarget, setEntryTarget] = useState<{ workspacePath: string; entryId: string; title: string } | null>(null) + const openWorkspaceEntry = (workspacePath: string, id: string) => { + const item = entryItems.find(entry => entry.id === id) + if (item === undefined) return + setEntryTarget({ workspacePath, entryId: id, title: item.label }) + } ... <SessionTree ... + entryItems={entryItems} + openWorkspaceEntry={openWorkspaceEntry} ... <Modal open={entryTarget !== null} onClose={() => { setEntryTarget(null) }} closeLabel={t('close')} title={entryTarget?.title ?? ''} > {entryTarget !== null && renderSlot('sidebar.workspaces.entry', { workspacePath: entryTarget.workspacePath }, { only: entryTarget.entryId })} </Modal>2. apiproxy —
skill.listexposessource(for scope filtering)The skills-manager plugin needs to distinguish "global (user-level) skills" from "workspace (project-level) skills", but the existing
skill.listwire projection deliberately doesn't exposesource(see the original comment "provider/source vocabulary stays host-side"). This is a minimal relaxation, avoiding adding a listing RPC per scope:Implementation progress (verified locally)
sidebar.workspaces.entrylist slot +slots.entries()projection + modal rendering: landed per the patch above;tsc+ 121 tests pass..skillsModal { width: min(760px, 92vw) }) so the toolbar's "Disable selected" etc. no longer wrap/overflow at the default 380px width.skill.list'scwdbranch (a workspace lists directly by path, no longer depending on an attached session); see the skill-list-source draft.Questions to confirm
listslot +slots.entries()projection + modal rendering direction match your plan for "workspace UI extensibility"? Or is there a more suitable seam I've missed?ui-workspacedeclaration/rendering + a minimal demo plugin + tests).Related
All reactions