From 2c23bca256d7936bdab336c879e9270015f98069 Mon Sep 17 00:00:00 2001 From: Endri Bezati Date: Sun, 8 Nov 2026 08:50:08 +0100 Subject: [PATCH 1/5] refactor: a sidecar profile names what is its own; the toolkit derives the rest Phase 3 of the declutter plan. About half the keys every product wrote were identical or derivable from the ones beside them, so the toolkit now derives them, each as the products spelled it by hand: command ids from the GLSP client name and the operation prefix, operation kinds from the prefix, the GLSP client identity from the key, the CLI and sidecar setting keys, the eight run-driver setting keys, the undo suffix and the scope argument from their one shared spelling. A value the product gives wins, so a product that keeps every line is unchanged. Deprecated, accepted and ignored: the three command ids nothing registers (createAgentToolPolicyFile, chatAddViewerEditor, chatAddViewerTask), the two chat fields nothing reads (name, operationPrefix) and the toolkit's mcpEnabledSetting. The chat's built-in node slash commands are gone: a profile names its own or offers none. The webview's three network behaviours are on unless a product sets them off. Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv --- CHANGELOG.md | 25 +++ packages/diagram-client/src/navigation-ui.ts | 2 +- .../src/network-navigation-mouse-listener.ts | 2 +- packages/diagram-client/src/profile.ts | 3 + packages/diagram-client/src/property-panel.ts | 2 +- .../test/client-behavior-flags.test.ts | 20 +-- packages/extension-core/src/api.ts | 27 ++- .../src/extension/chat/edit-capability.ts | 9 +- .../test/edit-capability.test.ts | 12 +- .../sidecar-toolkit/src/chat-edit-backend.ts | 2 - .../src/sidecar-diagram-profile.ts | 169 ++++++++++++++---- .../test/derived-profile-inputs.test.ts | 142 +++++++++++++++ .../test/read-only-profile.test.ts | 4 +- .../test/sidecar-diagram-profile.test.ts | 5 +- 14 files changed, 355 insertions(+), 69 deletions(-) create mode 100644 packages/sidecar-toolkit/test/derived-profile-inputs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d93ed18..8add99c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,31 @@ platform API version. ### Changed +- **A sidecar profile names what is its own; the toolkit derives the rest.** + `SidecarProfileInput` no longer requires the command ids, the operation + kinds, the GLSP client identity, the CLI and sidecar setting keys, the + eight run-driver setting keys, the undo suffix or the scope argument: each + is derived as every product spelled it by hand (`.glsp.`, + `.sidecar.edit`, `.createEntityPort`, `glsp.`, + `cliCommand`, `sidecarCommand`, `runOutputDir` and the agent-tool keys, + ` ()`, `workflow`), and a value the product gives wins + (`resolveSidecarProfileInput`, `resolveSidecarCommandIds`). A product's + profile can lose about half its lines; one that keeps them all is unchanged. +- **Three command ids and three chat fields are deprecated**, accepted and + ignored: `createAgentToolPolicyFile`, `chatAddViewerEditor` and + `chatAddViewerTask` (nothing registers them), `chat.name` and + `chat.operationPrefix` (nothing reads them), and the toolkit input's + `mcpEnabledSetting` (the rollback lever is `dialogram.chat.useGlspMcp`). +- **The chat offers no node-creation slash command the profile did not name.** + The built-in `/create-task`, `/create-agent`, `/create-viewer` and + `/create-workflow` fallback is gone; every sidecar product names its own + `chat.nodeCommands` already. +- **The network behaviours of the webview are on by default**: + `graphSourceNavigation`, `networkPropertySections` and + `networkNavigationLabels` read as on unless the product sets `false`. Four + of five sidecar products set all three on; the workflow product sets them + off and keeps doing so. A consumer that set none, such as a library-mode + viewer, now gets the network vocabulary and drill-down. - **After a drop, the routes an edge cannot keep are drawn by libavoid, with the edges on one port as one shared path.** The layout draws the edges into a port as a trunk; the built-in router gives every edge its own channel and diff --git a/packages/diagram-client/src/navigation-ui.ts b/packages/diagram-client/src/navigation-ui.ts index 2fcf63d..294f306 100644 --- a/packages/diagram-client/src/navigation-ui.ts +++ b/packages/diagram-client/src/navigation-ui.ts @@ -223,7 +223,7 @@ function labelForParentWorkflow(option: { sourceUri: string; workflowName: strin } function navigationEntityLabel(plural = false): string { - if (clientBehavior().networkNavigationLabels === true) { + if (clientBehavior().networkNavigationLabels !== false) { return plural ? 'Networks' : 'Network'; } return plural ? 'Workflows' : 'Workflow'; diff --git a/packages/diagram-client/src/network-navigation-mouse-listener.ts b/packages/diagram-client/src/network-navigation-mouse-listener.ts index 174f4ff..b7aaaa9 100644 --- a/packages/diagram-client/src/network-navigation-mouse-listener.ts +++ b/packages/diagram-client/src/network-navigation-mouse-listener.ts @@ -199,7 +199,7 @@ export class WorkflowNetworkNavigationMouseListener extends MouseListener implem const selectedRunId = typeof (globalThis as any)?.__calDiagramContext?.selectedRunId === 'string' ? String((globalThis as any).__calDiagramContext.selectedRunId).trim() || undefined : undefined; - const useGraphSourceNavigation = clientBehavior().graphSourceNavigation === true; + const useGraphSourceNavigation = clientBehavior().graphSourceNavigation !== false; // No-op guard: avoid dispatching when drill-down resolves to the workflow // currently displayed in the same document. diff --git a/packages/diagram-client/src/profile.ts b/packages/diagram-client/src/profile.ts index 7e242f4..4914d11 100644 --- a/packages/diagram-client/src/profile.ts +++ b/packages/diagram-client/src/profile.ts @@ -15,8 +15,11 @@ type DiagramOperationKinds = { }; type DiagramClientBehavior = { + /** On unless the product sets `false`. */ graphSourceNavigation?: boolean; + /** On unless the product sets `false`. */ networkPropertySections?: boolean; + /** On unless the product sets `false`. */ networkNavigationLabels?: boolean; noneSentinel?: string; scriptInterpreterCommands?: string[]; diff --git a/packages/diagram-client/src/property-panel.ts b/packages/diagram-client/src/property-panel.ts index 029916d..254b62f 100644 --- a/packages/diagram-client/src/property-panel.ts +++ b/packages/diagram-client/src/property-panel.ts @@ -170,7 +170,7 @@ export class PropertyPanel implements ISelectionListener, IGModelRootListener { private lastRenderedAgentHistoryJson: string | undefined; protected isNetworkRuntime(): boolean { - return clientBehavior().networkPropertySections === true; + return clientBehavior().networkPropertySections !== false; } // ── IGModelRootListener ───────────────────────────────────────────── diff --git a/packages/diagram-client/test/client-behavior-flags.test.ts b/packages/diagram-client/test/client-behavior-flags.test.ts index 84bd90c..2e7d351 100644 --- a/packages/diagram-client/test/client-behavior-flags.test.ts +++ b/packages/diagram-client/test/client-behavior-flags.test.ts @@ -41,11 +41,11 @@ describe('clientBehavior flag consumption', () => { inject(WFPY_BEHAVIOR); const cb = clientBehavior(); // network-navigation-mouse-listener drill-down predicate - expect(cb.graphSourceNavigation === true).toBe(false); + expect(cb.graphSourceNavigation !== false).toBe(false); // property-panel isNetworkRuntime predicate - expect(cb.networkPropertySections === true).toBe(false); + expect(cb.networkPropertySections !== false).toBe(false); // navigation-ui entity-label predicate - expect(cb.networkNavigationLabels === true).toBe(false); + expect(cb.networkNavigationLabels !== false).toBe(false); // property-panel CLI-tools "none" sentinel expect(cb.noneSentinel).toBe('wfpy-none'); // views.ts icon detection @@ -57,19 +57,19 @@ describe('clientBehavior flag consumption', () => { it('resolves the calpy truth table (network behaviors on)', () => { inject(CALPY_BEHAVIOR); const cb = clientBehavior(); - expect(cb.graphSourceNavigation === true).toBe(true); - expect(cb.networkPropertySections === true).toBe(true); - expect(cb.networkNavigationLabels === true).toBe(true); + expect(cb.graphSourceNavigation !== false).toBe(true); + expect(cb.networkPropertySections !== false).toBe(true); + expect(cb.networkNavigationLabels !== false).toBe(true); expect(cb.noneSentinel).toBe('wfpy-none'); expect((cb.scriptInterpreterCommands ?? []).includes('python')).toBe(true); expect((cb.scriptInterpreterCommands ?? []).includes('python3')).toBe(true); }); - it('reads as all-off with no sentinel when nothing is injected (dev/standalone)', () => { + it('reads the network behaviours as on, with no sentinel, when nothing is injected (a product that sets none, dev/standalone)', () => { const cb = clientBehavior(); - expect(cb.graphSourceNavigation === true).toBe(false); - expect(cb.networkPropertySections === true).toBe(false); - expect(cb.networkNavigationLabels === true).toBe(false); + expect(cb.graphSourceNavigation !== false).toBe(true); + expect(cb.networkPropertySections !== false).toBe(true); + expect(cb.networkNavigationLabels !== false).toBe(true); expect(cb.noneSentinel).toBeUndefined(); expect((cb.scriptInterpreterCommands ?? []).includes('python')).toBe(false); }); diff --git a/packages/extension-core/src/api.ts b/packages/extension-core/src/api.ts index ccc29e5..f2ad051 100644 --- a/packages/extension-core/src/api.ts +++ b/packages/extension-core/src/api.ts @@ -72,9 +72,12 @@ export interface DiagramCommandIds { layoutDiagramIfNeeded: string; setAgentToolConfig: string; getAgentToolConfig: string; - createAgentToolPolicyFile: string; - chatAddViewerEditor: string; - chatAddViewerTask: string; + /** @deprecated Nothing registers or runs it; accepted and ignored. */ + createAgentToolPolicyFile?: string; + /** @deprecated Nothing registers or runs it; accepted and ignored. */ + chatAddViewerEditor?: string; + /** @deprecated Nothing registers or runs it; accepted and ignored. */ + chatAddViewerTask?: string; createNewContainer: string; } @@ -126,11 +129,14 @@ export interface DiagramStorageOptions { * profiles assembled inside the platform bundle) never populates them. */ export interface DiagramChatConfig { - name: string; + /** @deprecated Nothing reads it; accepted and ignored. */ + name?: string; fullName: string; - /** Internal op-dispatcher namespace shared by the chat backend and intent resolver. */ - operationPrefix: string; + /** @deprecated Nothing reads it (the storage options carry their own prefix); accepted and ignored. */ + operationPrefix?: string; skill?: string; + /** The slash commands that create a node, one per node type the product has. + * Absent: the chat offers none. */ nodeCommands?: Array<{ command: string; nodeType: string; @@ -351,11 +357,14 @@ export interface DiagramClientBehavior { /** The ACP connectors the property panel offers on an agent's `connector`; * listed per document by the platform from {@link DiagramChatConfig.acpConnector}. */ acpConnectors?: AcpConnectorInfo[]; - /** Cross-file drill-down navigation resolves through the graph source model. */ + /** Cross-file drill-down navigation resolves through the graph source model. + * On by default; a product whose navigation runs on the file's text sets `false`. */ graphSourceNavigation?: boolean; - /** Property panel renders the network-model sections and labels. */ + /** Property panel renders the network-model sections and labels. + * On by default; a product whose model is a workflow sets `false`. */ networkPropertySections?: boolean; - /** Cross-file navigation UI uses the network entity vocabulary. */ + /** Cross-file navigation UI uses the network entity vocabulary. + * On by default; a product whose model is a workflow sets `false`. */ networkNavigationLabels?: boolean; /** Sentinel option value/default for the agent CLI-tools "none" selection. */ noneSentinel?: string; diff --git a/packages/extension-core/src/extension/chat/edit-capability.ts b/packages/extension-core/src/extension/chat/edit-capability.ts index cfee98f..eddd819 100644 --- a/packages/extension-core/src/extension/chat/edit-capability.ts +++ b/packages/extension-core/src/extension/chat/edit-capability.ts @@ -36,13 +36,6 @@ export interface EditChatCapability { } /** Default node-creation commands when the chat config supplies none. */ -const DEFAULT_NODE_COMMANDS: Array<{ command: string; nodeType: string; description: string }> = [ - { command: 'create-task', nodeType: 'task', description: 'Create a new task node' }, - { command: 'create-agent', nodeType: 'agent', description: 'Create a new agent node' }, - { command: 'create-viewer', nodeType: 'viewer', description: 'Create a new viewer node' }, - { command: 'create-workflow', nodeType: 'workflow', description: 'Create a new workflow node' } -]; - const AUTO_LAYOUT_DEBOUNCE_MS = 400; const VIEW_OP_SETTLE_MS = 500; @@ -112,7 +105,7 @@ export function createEditChatCapability(deps: EditChatCapabilityDeps): EditChat const slashCommands: ChatCommandContribution[] = []; - for (const nc of profile.chat?.nodeCommands ?? DEFAULT_NODE_COMMANDS) { + for (const nc of profile.chat?.nodeCommands ?? []) { slashCommands.push({ command: nc.command, description: nc.description, diff --git a/packages/extension-core/test/edit-capability.test.ts b/packages/extension-core/test/edit-capability.test.ts index 37cf278..84083f7 100644 --- a/packages/extension-core/test/edit-capability.test.ts +++ b/packages/extension-core/test/edit-capability.test.ts @@ -25,7 +25,15 @@ function makeProfile() { center: 'workflow.glsp.center', fitToScreen: 'workflow.glsp.fitToScreen' }, - chat: { name: 'wf', fullName: 'Workflow Chat', operationPrefix: 'workflow' } + chat: { + fullName: 'Workflow Chat', + nodeCommands: [ + { command: 'create-task', nodeType: 'task', description: 'Create a new task node' }, + { command: 'create-agent', nodeType: 'agent', description: 'Create a new agent node' }, + { command: 'create-viewer', nodeType: 'viewer', description: 'Create a new viewer node' }, + { command: 'create-workflow', nodeType: 'workflow', description: 'Create a new workflow node' } + ] + } } as any; } @@ -56,7 +64,7 @@ beforeEach(() => { }); describe('slash contributions', () => { - it('contributes default node commands, edit commands and /layout', () => { + it('contributes the profile\'s node commands, edit commands and /layout', () => { const { capability } = makeCapability(); const names = capability.slashCommands.map(c => c.command); expect(names).toEqual( diff --git a/packages/sidecar-toolkit/src/chat-edit-backend.ts b/packages/sidecar-toolkit/src/chat-edit-backend.ts index 627f99f..617ad71 100644 --- a/packages/sidecar-toolkit/src/chat-edit-backend.ts +++ b/packages/sidecar-toolkit/src/chat-edit-backend.ts @@ -24,8 +24,6 @@ export interface SidecarEditBackendConfig { sidecarOperationPrefix: string; /** Graph-export op name (without prefix); differs per runtime. */ exportOp?: string; - /** Kill-switch setting for the MCP tool server. */ - mcpEnabledSetting: { section: string; key: string; default: boolean }; /** Arg key used to scope ops to a sub-graph (today `'workflow'`). */ scopeArgKey: string; } diff --git a/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts b/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts index e6ae486..81a9775 100644 --- a/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts +++ b/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts @@ -93,7 +93,13 @@ export interface SidecarClientBehavior { outlineTitle?: string; } -/** The consumer-owned command ids a sidecar diagram profile carries. */ +/** + * The command ids a sidecar diagram profile carries. A product may name any of + * them; the ones it leaves out are derived (see {@link resolveSidecarProfileInput}): + * `.glsp.` for the diagram commands, and + * `.sidecar.edit` / `.send` for the two raw sidecar + * commands. The derived id is what every product spelled by hand before. + */ export interface SidecarProfileCommandIds { openDiagram: string; openDiagramSplit: string; @@ -112,9 +118,12 @@ export interface SidecarProfileCommandIds { layoutDiagramIfNeeded: string; setAgentToolConfig: string; getAgentToolConfig: string; - createAgentToolPolicyFile: string; - chatAddViewerEditor: string; - chatAddViewerTask: string; + /** @deprecated Nothing registers or runs it; accepted and ignored. */ + createAgentToolPolicyFile?: string; + /** @deprecated Nothing registers or runs it; accepted and ignored. */ + chatAddViewerEditor?: string; + /** @deprecated Nothing registers or runs it; accepted and ignored. */ + chatAddViewerTask?: string; sidecarEdit: string; sidecarSend: string; createNewContainer: string; @@ -142,10 +151,14 @@ export interface SidecarProfileInput { displayName: string; settingsNamespace: string; customEditorViewType: string; - glspClientId: string; - glspClientName: string; - commands: SidecarProfileCommandIds; - operationKinds: { createEntityPort: string; deleteEntityPort: string }; + /** Default `glsp.`. */ + glspClientId?: string; + /** The first segment of the derived command ids. Default `key`. */ + glspClientName?: string; + /** Command ids the product spells itself; the rest are derived (see {@link SidecarProfileCommandIds}). */ + commands?: Partial; + /** Default `.createEntityPort` / `.deleteEntityPort`. */ + operationKinds?: { createEntityPort: string; deleteEntityPort: string }; /** Neutral behavior flags forwarded into the diagram webview. */ clientBehavior?: SidecarClientBehavior; /** @@ -178,14 +191,17 @@ export interface SidecarProfileInput { // Sidecar/CLI transport. sidecarOperationPrefix: string; - /** The setting and default naming the sidecar command. Required unless - * `readOnly` (assembly throws without them otherwise). */ + /** The setting and default naming the sidecar command. The default is + * required unless `readOnly` (assembly throws without it otherwise); the + * setting key defaults to `sidecarCommand` when the default is given. */ sidecarCommandSettingKey?: string; sidecarCommandDefault?: string; - cliCommandSettingKey: string; + /** The setting naming the CLI command, under `settingsNamespace`. Default `cliCommand`. */ + cliCommandSettingKey?: string; cliCommandDefault: string; cliPythonModule?: string; - acceptedOperationPrefixes: string[]; + /** Default `[sidecarOperationPrefix]`. */ + acceptedOperationPrefixes?: string[]; graphAcquisition: 'sidecar-export' | 'cli-plan'; cliGraphArgs?: (file: string, requestedWorkflow?: string) => string[]; /** @@ -204,7 +220,8 @@ export interface SidecarProfileInput { cliResumeArgs?: (runDir: string, step: number) => string[]; // User-visible strings / behavior. - undoLabelSuffix: string; + /** Appended to every undo label. Default ` ()`. */ + undoLabelSuffix?: string; createNodeStrings: CreateNodeStrings; createNodeBehavior: CreateNodeBehavior; createNodeVariants?: CreateNodeVariant[]; @@ -243,8 +260,10 @@ export interface SidecarProfileInput { // Chat edit backend. exportOp?: string; - mcpEnabledSetting: { section: string; key: string; default: boolean }; - scopeArgKey: string; + /** @deprecated Nothing reads it (the MCP rollback lever is the platform's `dialogram.chat.useGlspMcp`); accepted and ignored. */ + mcpEnabledSetting?: { section: string; key: string; default: boolean }; + /** The op argument that scopes an edit to a sub-graph. Default `workflow`, the sidecar protocol's name for it. */ + scopeArgKey?: string; /** * The Java ELK server the product ships, for large graphs: its fat jar, @@ -306,15 +325,18 @@ export interface SidecarProfileInput { */ statusSource?: SidecarStatusSourceInput; - // Run driver. - runOutputDirSettingKey: string; - liveExecutionGlowSettingKey: string; - agentToolsSettingKey: string; - agentToolAuthSettingKey: string; - agentToolPolicySettingKey: string; - agentToolTimeoutMsSettingKey: string; - agentToolRegistrySettingKey: string; - agentMcpBridgeCmdSettingKey: string; + // Run driver. The settings it reads under `settingsNamespace`; each + // defaults to the key every product declares: `runOutputDir`, + // `liveExecutionGlow`, `agentTools`, `agentToolAuth`, `agentToolPolicy`, + // `agentToolTimeoutMs`, `agentToolRegistry`, `agentMcpBridgeCmd`. + runOutputDirSettingKey?: string; + liveExecutionGlowSettingKey?: string; + agentToolsSettingKey?: string; + agentToolAuthSettingKey?: string; + agentToolPolicySettingKey?: string; + agentToolTimeoutMsSettingKey?: string; + agentToolRegistrySettingKey?: string; + agentMcpBridgeCmdSettingKey?: string; /** The setting naming the ACP connector (`.`, e.g. * `acp.connector`): the one the chat talks to, offered by the property * panel, and, with `runAcpFlags` on, the run's default (`--acp-connector`). @@ -334,7 +356,8 @@ export interface SidecarProfileInput { // Chat carry-overs. chat: { - name: string; + /** @deprecated Nothing reads it; accepted and ignored. */ + name?: string; fullName: string; skill?: string; nodeCommands?: Array<{ command: string; nodeType: string; description: string }>; @@ -356,6 +379,92 @@ export interface SidecarProfileInput { watchGlobs?: string[]; } +/** The fields {@link resolveSidecarProfileInput} fills in when the product leaves them out. */ +type DerivedSidecarProfileFields = + | 'glspClientId' | 'glspClientName' | 'operationKinds' | 'cliCommandSettingKey' | 'acceptedOperationPrefixes' + | 'undoLabelSuffix' | 'scopeArgKey' + | 'runOutputDirSettingKey' | 'liveExecutionGlowSettingKey' | 'agentToolsSettingKey' | 'agentToolAuthSettingKey' + | 'agentToolPolicySettingKey' | 'agentToolTimeoutMsSettingKey' | 'agentToolRegistrySettingKey' | 'agentMcpBridgeCmdSettingKey'; + +/** A {@link SidecarProfileInput} with every derivable field filled in. */ +export type ResolvedSidecarProfileInput = + Omit + & Required> + & { commands: SidecarProfileCommandIds }; + +/** The command ids `input` leaves out, derived as every product spelled them by hand. */ +export function resolveSidecarCommandIds( + input: Pick +): SidecarProfileCommandIds { + const client = input.glspClientName ?? input.key; + const glsp = (field: string): string => `${client}.glsp.${field}`; + const given = input.commands ?? {}; + const derived: SidecarProfileCommandIds = { + openDiagram: glsp('openDiagram'), + openDiagramSplit: glsp('openDiagramSplit'), + layoutDiagram: glsp('layoutDiagram'), + refreshDiagramModel: glsp('refreshDiagramModel'), + renameEntityByName: glsp('renameEntityByName'), + undo: glsp('undo'), + redo: glsp('redo'), + fitToScreen: glsp('fitToScreen'), + center: glsp('center'), + exportSvg: glsp('exportSvg'), + toggleGrid: glsp('toggleGrid'), + setQueueTraceVisible: glsp('setQueueTraceVisible'), + stopWorkflow: glsp('stopWorkflow'), + runWorkflow: glsp('runWorkflow'), + layoutDiagramIfNeeded: glsp('layoutDiagramIfNeeded'), + setAgentToolConfig: glsp('setAgentToolConfig'), + getAgentToolConfig: glsp('getAgentToolConfig'), + sidecarEdit: `${input.sidecarOperationPrefix}.sidecar.edit`, + sidecarSend: `${input.sidecarOperationPrefix}.sidecar.send`, + createNewContainer: glsp('createNewContainer') + }; + const resolved: Record = { ...derived }; + for (const [field, id] of Object.entries(given)) { + if (typeof id === 'string' && id.trim() !== '') { + resolved[field] = id; + } + } + return resolved as unknown as SidecarProfileCommandIds; +} + +/** + * `input` with every field a product may leave out filled in: the command + * ids, the operation kinds, the GLSP client identity, the setting keys and + * the undo suffix, each as the products spelled them by hand. A value the + * product gives wins. + */ +export function resolveSidecarProfileInput(input: SidecarProfileInput): ResolvedSidecarProfileInput { + const glspClientName = input.glspClientName ?? input.key; + const prefix = input.sidecarOperationPrefix; + return { + ...input, + glspClientName, + glspClientId: input.glspClientId ?? `glsp.${glspClientName}`, + commands: resolveSidecarCommandIds(input), + operationKinds: input.operationKinds ?? { + createEntityPort: `${prefix}.createEntityPort`, + deleteEntityPort: `${prefix}.deleteEntityPort` + }, + sidecarCommandSettingKey: input.sidecarCommandSettingKey + ?? (input.sidecarCommandDefault !== undefined ? 'sidecarCommand' : undefined), + cliCommandSettingKey: input.cliCommandSettingKey ?? 'cliCommand', + acceptedOperationPrefixes: input.acceptedOperationPrefixes ?? [prefix], + undoLabelSuffix: input.undoLabelSuffix ?? ` (${input.key})`, + scopeArgKey: input.scopeArgKey ?? 'workflow', + runOutputDirSettingKey: input.runOutputDirSettingKey ?? 'runOutputDir', + liveExecutionGlowSettingKey: input.liveExecutionGlowSettingKey ?? 'liveExecutionGlow', + agentToolsSettingKey: input.agentToolsSettingKey ?? 'agentTools', + agentToolAuthSettingKey: input.agentToolAuthSettingKey ?? 'agentToolAuth', + agentToolPolicySettingKey: input.agentToolPolicySettingKey ?? 'agentToolPolicy', + agentToolTimeoutMsSettingKey: input.agentToolTimeoutMsSettingKey ?? 'agentToolTimeoutMs', + agentToolRegistrySettingKey: input.agentToolRegistrySettingKey ?? 'agentToolRegistry', + agentMcpBridgeCmdSettingKey: input.agentMcpBridgeCmdSettingKey ?? 'agentMcpBridgeCmd' + }; +} + /** The product's source extensions: `sourceExtensions` when given, else the one `sourceExtension`. */ export function sourceExtensionsOf(input: Pick): string[] { const many = (input.sourceExtensions ?? []).map(ext => ext.trim()).filter(ext => ext !== ''); @@ -366,7 +475,7 @@ export function sourceExtensionsOf(input: Pick; } -export function createSidecarDiagramProfile(input: SidecarProfileInput) { +export function createSidecarDiagramProfile(givenInput: SidecarProfileInput) { + const input = resolveSidecarProfileInput(givenInput); // A read-only profile has no sidecar, so the sidecar fields may be absent // and nothing may need one; an editable profile must name its sidecar. // Both are checked here, where the message can say which field and why, @@ -489,7 +599,6 @@ export function createSidecarDiagramProfile(input: SidecarProfileInput) { sidecarCommandDefault: sidecarCommand.commandDefault, sidecarOperationPrefix: input.sidecarOperationPrefix, exportOp: input.exportOp, - mcpEnabledSetting: input.mcpEnabledSetting, scopeArgKey: input.scopeArgKey }) : undefined; @@ -700,9 +809,7 @@ export function createSidecarDiagramProfile(input: SidecarProfileInput) { mcp: { enabled: true }, elkServer: input.elkServer, chat: { - name: input.chat.name, fullName: input.chat.fullName, - operationPrefix: input.sidecarOperationPrefix, skill: input.chat.skill, nodeCommands: input.chat.nodeCommands, // Read-only sidecar-registry tools bridged to GLSP-MCP by the platform adapter. diff --git a/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts b/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts new file mode 100644 index 0000000..b2f9588 --- /dev/null +++ b/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts @@ -0,0 +1,142 @@ +/** + * A product names what is its own; the toolkit derives the rest the way every + * product spelled it by hand: command ids from the GLSP client name, operation + * kinds and the raw sidecar commands from the operation prefix, the setting + * keys and the undo suffix from their one shared spelling. + */ +import { describe, expect, it } from 'vitest'; +import { + createSidecarDiagramProfile, + resolveSidecarCommandIds, + resolveSidecarProfileInput, + type SidecarProfileInput +} from '../src/sidecar-diagram-profile'; +import { NEUTRAL_CREATE_NODE_CONFIG } from './fixtures/create-node-config.js'; + +/** What a Verilog product has to say, and nothing more. */ +function minimalInput(overrides: Partial = {}): SidecarProfileInput { + return { + key: 'sv', + displayName: 'SystemVerilog IDE', + settingsNamespace: 'sv', + customEditorViewType: 'sv.moduleDiagram', + sidecarOperationPrefix: 'sv', + sidecarCommandDefault: 'sv-sidecar', + cliCommandDefault: 'hdl', + graphAcquisition: 'cli-plan', + cliGraphArgs: file => ['plan', file], + sourceExtension: '.sv', + newContainer: { label: 'Module', decorator: 'module', importLine: '' }, + chat: { fullName: 'SystemVerilog' }, + createNodeStrings: NEUTRAL_CREATE_NODE_CONFIG.createNodeStrings, + createNodeBehavior: NEUTRAL_CREATE_NODE_CONFIG.createNodeBehavior, + ...overrides + }; +} + +describe('resolveSidecarCommandIds', () => { + it('derives every id as the products spelled them', () => { + expect(resolveSidecarCommandIds({ key: 'sv', sidecarOperationPrefix: 'sv' })).toEqual({ + openDiagram: 'sv.glsp.openDiagram', + openDiagramSplit: 'sv.glsp.openDiagramSplit', + layoutDiagram: 'sv.glsp.layoutDiagram', + refreshDiagramModel: 'sv.glsp.refreshDiagramModel', + renameEntityByName: 'sv.glsp.renameEntityByName', + undo: 'sv.glsp.undo', + redo: 'sv.glsp.redo', + fitToScreen: 'sv.glsp.fitToScreen', + center: 'sv.glsp.center', + exportSvg: 'sv.glsp.exportSvg', + toggleGrid: 'sv.glsp.toggleGrid', + setQueueTraceVisible: 'sv.glsp.setQueueTraceVisible', + stopWorkflow: 'sv.glsp.stopWorkflow', + runWorkflow: 'sv.glsp.runWorkflow', + layoutDiagramIfNeeded: 'sv.glsp.layoutDiagramIfNeeded', + setAgentToolConfig: 'sv.glsp.setAgentToolConfig', + getAgentToolConfig: 'sv.glsp.getAgentToolConfig', + sidecarEdit: 'sv.sidecar.edit', + sidecarSend: 'sv.sidecar.send', + createNewContainer: 'sv.glsp.createNewContainer' + }); + }); + + it('takes the GLSP client name for the diagram commands and the operation prefix for the sidecar ones', () => { + const ids = resolveSidecarCommandIds({ key: 'wfpy', glspClientName: 'workflow', sidecarOperationPrefix: 'wfpy' }); + expect(ids.openDiagram).toBe('workflow.glsp.openDiagram'); + expect(ids.sidecarEdit).toBe('wfpy.sidecar.edit'); + }); + + it('keeps the ids the product names and ignores blank ones', () => { + const ids = resolveSidecarCommandIds({ + key: 'sv', + sidecarOperationPrefix: 'sv', + commands: { createNewContainer: 'sv.glsp.createNewModule', undo: ' ' } + }); + expect(ids.createNewContainer).toBe('sv.glsp.createNewModule'); + expect(ids.undo).toBe('sv.glsp.undo'); + }); +}); + +describe('resolveSidecarProfileInput', () => { + it('fills the derivable fields from the key and the operation prefix', () => { + const resolved = resolveSidecarProfileInput(minimalInput()); + expect(resolved.glspClientName).toBe('sv'); + expect(resolved.glspClientId).toBe('glsp.sv'); + expect(resolved.operationKinds).toEqual({ createEntityPort: 'sv.createEntityPort', deleteEntityPort: 'sv.deleteEntityPort' }); + expect(resolved.acceptedOperationPrefixes).toEqual(['sv']); + expect(resolved.sidecarCommandSettingKey).toBe('sidecarCommand'); + expect(resolved.cliCommandSettingKey).toBe('cliCommand'); + expect(resolved.undoLabelSuffix).toBe(' (sv)'); + expect(resolved.scopeArgKey).toBe('workflow'); + expect(resolved.runOutputDirSettingKey).toBe('runOutputDir'); + expect(resolved.liveExecutionGlowSettingKey).toBe('liveExecutionGlow'); + expect(resolved.agentToolsSettingKey).toBe('agentTools'); + expect(resolved.agentToolAuthSettingKey).toBe('agentToolAuth'); + expect(resolved.agentToolPolicySettingKey).toBe('agentToolPolicy'); + expect(resolved.agentToolTimeoutMsSettingKey).toBe('agentToolTimeoutMs'); + expect(resolved.agentToolRegistrySettingKey).toBe('agentToolRegistry'); + expect(resolved.agentMcpBridgeCmdSettingKey).toBe('agentMcpBridgeCmd'); + }); + + it('leaves the sidecar setting key out when there is no sidecar to name', () => { + const resolved = resolveSidecarProfileInput(minimalInput({ readOnly: true, sidecarCommandDefault: undefined })); + expect(resolved.sidecarCommandSettingKey).toBeUndefined(); + }); + + it('lets every given value win', () => { + const resolved = resolveSidecarProfileInput(minimalInput({ + glspClientName: 'workflow', + glspClientId: 'glsp.custom', + operationKinds: { createEntityPort: 'x.create', deleteEntityPort: 'x.delete' }, + acceptedOperationPrefixes: ['wfpy', 'calpy'], + undoLabelSuffix: ' (custom)', + cliCommandSettingKey: 'cli', + runOutputDirSettingKey: 'outDir' + })); + expect(resolved.glspClientId).toBe('glsp.custom'); + expect(resolved.commands.openDiagram).toBe('workflow.glsp.openDiagram'); + expect(resolved.operationKinds.createEntityPort).toBe('x.create'); + expect(resolved.acceptedOperationPrefixes).toEqual(['wfpy', 'calpy']); + expect(resolved.undoLabelSuffix).toBe(' (custom)'); + expect(resolved.cliCommandSettingKey).toBe('cli'); + expect(resolved.runOutputDirSettingKey).toBe('outDir'); + }); +}); + +describe('createSidecarDiagramProfile from a minimal input', () => { + it('assembles a profile whose ids are the derived ones', () => { + const profile = createSidecarDiagramProfile(minimalInput()); + expect(profile.commands.openDiagram).toBe('sv.glsp.openDiagram'); + expect(profile.commands.createNewContainer).toBe('sv.glsp.createNewContainer'); + expect(profile.glspClientId).toBe('glsp.sv'); + expect(profile.glspClientName).toBe('sv'); + expect(profile.operationKinds).toEqual({ createEntityPort: 'sv.createEntityPort', deleteEntityPort: 'sv.deleteEntityPort' }); + }); + + it('passes the chat on without the fields nothing reads', () => { + const profile = createSidecarDiagramProfile(minimalInput({ chat: { name: 'legacy', fullName: 'SystemVerilog' } })); + expect(profile.chat?.fullName).toBe('SystemVerilog'); + expect((profile.chat as any).name).toBeUndefined(); + expect((profile.chat as any).operationPrefix).toBeUndefined(); + }); +}); diff --git a/packages/sidecar-toolkit/test/read-only-profile.test.ts b/packages/sidecar-toolkit/test/read-only-profile.test.ts index a32a974..7b4d7f4 100644 --- a/packages/sidecar-toolkit/test/read-only-profile.test.ts +++ b/packages/sidecar-toolkit/test/read-only-profile.test.ts @@ -244,8 +244,8 @@ describe('createSidecarDiagramProfile with readOnly', () => { .toThrow(/openability\.probeOp/); expect(() => createSidecarDiagramProfile(editableInput({ sidecarCommandDefault: undefined }))) .toThrow(/sidecarCommandSettingKey and sidecarCommandDefault are required/); - expect(() => createSidecarDiagramProfile(editableInput({ sidecarCommandSettingKey: undefined }))) - .toThrow(/required unless readOnly/); + // The setting key alone is derived (`sidecarCommand`) once the default names a sidecar. + expect(createSidecarDiagramProfile(editableInput({ sidecarCommandSettingKey: undefined }))).toBeDefined(); }); }); diff --git a/packages/sidecar-toolkit/test/sidecar-diagram-profile.test.ts b/packages/sidecar-toolkit/test/sidecar-diagram-profile.test.ts index 7412424..9d46df6 100644 --- a/packages/sidecar-toolkit/test/sidecar-diagram-profile.test.ts +++ b/packages/sidecar-toolkit/test/sidecar-diagram-profile.test.ts @@ -176,9 +176,10 @@ describe('createSidecarDiagramProfile', () => { ).toBe(false); }); - it('threads the sidecar operation prefix onto the chat carry-over', () => { + it('threads the sidecar operation prefix onto the storage options, and no longer onto the chat (nothing read it there)', () => { const p = createSidecarDiagramProfile(baseInput({ sidecarOperationPrefix: 'calpy' })); - expect(p.chat?.operationPrefix).toBe('calpy'); + expect(p.storageOptions?.operationPrefix).toBe('calpy'); + expect(p.chat?.operationPrefix).toBeUndefined(); }); it('omits the openability check when the input requests none (always openable)', () => { From 1705dd4f1f3fe361620847997d3b455ea1c85019 Mon Sep 17 00:00:00 2001 From: Endri Bezati Date: Sun, 8 Nov 2026 08:57:38 +0100 Subject: [PATCH 2/5] fix: what the review of the profile defaults turned up The webview's three network flags keep their off default: ndf-ide sets none of them, and on by default would have given its edges a queue capacity editor and changed its cross-document navigation. Their default is a design question for the product pass, not this refactor. A deprecated command id a product still spells stays out of the resolved profile (only the ids the platform registers come through), a blank GLSP client name, client id, setting key or suffix is derived like an absent one, the chat config's doc no longer names a dispatcher that is gone, and a test pins that a profile without node commands offers no node-creation slash command. Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv --- CHANGELOG.md | 6 --- packages/diagram-client/src/navigation-ui.ts | 2 +- .../src/network-navigation-mouse-listener.ts | 2 +- packages/diagram-client/src/profile.ts | 3 -- packages/diagram-client/src/property-panel.ts | 2 +- .../test/client-behavior-flags.test.ts | 20 ++++---- packages/extension-core/src/api.ts | 19 ++++---- .../test/edit-capability.test.ts | 13 ++++- .../src/sidecar-diagram-profile.ts | 47 ++++++++++--------- .../test/derived-profile-inputs.test.ts | 17 +++++++ 10 files changed, 75 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8add99c..5a04564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,12 +67,6 @@ platform API version. The built-in `/create-task`, `/create-agent`, `/create-viewer` and `/create-workflow` fallback is gone; every sidecar product names its own `chat.nodeCommands` already. -- **The network behaviours of the webview are on by default**: - `graphSourceNavigation`, `networkPropertySections` and - `networkNavigationLabels` read as on unless the product sets `false`. Four - of five sidecar products set all three on; the workflow product sets them - off and keeps doing so. A consumer that set none, such as a library-mode - viewer, now gets the network vocabulary and drill-down. - **After a drop, the routes an edge cannot keep are drawn by libavoid, with the edges on one port as one shared path.** The layout draws the edges into a port as a trunk; the built-in router gives every edge its own channel and diff --git a/packages/diagram-client/src/navigation-ui.ts b/packages/diagram-client/src/navigation-ui.ts index 294f306..2fcf63d 100644 --- a/packages/diagram-client/src/navigation-ui.ts +++ b/packages/diagram-client/src/navigation-ui.ts @@ -223,7 +223,7 @@ function labelForParentWorkflow(option: { sourceUri: string; workflowName: strin } function navigationEntityLabel(plural = false): string { - if (clientBehavior().networkNavigationLabels !== false) { + if (clientBehavior().networkNavigationLabels === true) { return plural ? 'Networks' : 'Network'; } return plural ? 'Workflows' : 'Workflow'; diff --git a/packages/diagram-client/src/network-navigation-mouse-listener.ts b/packages/diagram-client/src/network-navigation-mouse-listener.ts index b7aaaa9..174f4ff 100644 --- a/packages/diagram-client/src/network-navigation-mouse-listener.ts +++ b/packages/diagram-client/src/network-navigation-mouse-listener.ts @@ -199,7 +199,7 @@ export class WorkflowNetworkNavigationMouseListener extends MouseListener implem const selectedRunId = typeof (globalThis as any)?.__calDiagramContext?.selectedRunId === 'string' ? String((globalThis as any).__calDiagramContext.selectedRunId).trim() || undefined : undefined; - const useGraphSourceNavigation = clientBehavior().graphSourceNavigation !== false; + const useGraphSourceNavigation = clientBehavior().graphSourceNavigation === true; // No-op guard: avoid dispatching when drill-down resolves to the workflow // currently displayed in the same document. diff --git a/packages/diagram-client/src/profile.ts b/packages/diagram-client/src/profile.ts index 4914d11..7e242f4 100644 --- a/packages/diagram-client/src/profile.ts +++ b/packages/diagram-client/src/profile.ts @@ -15,11 +15,8 @@ type DiagramOperationKinds = { }; type DiagramClientBehavior = { - /** On unless the product sets `false`. */ graphSourceNavigation?: boolean; - /** On unless the product sets `false`. */ networkPropertySections?: boolean; - /** On unless the product sets `false`. */ networkNavigationLabels?: boolean; noneSentinel?: string; scriptInterpreterCommands?: string[]; diff --git a/packages/diagram-client/src/property-panel.ts b/packages/diagram-client/src/property-panel.ts index 254b62f..029916d 100644 --- a/packages/diagram-client/src/property-panel.ts +++ b/packages/diagram-client/src/property-panel.ts @@ -170,7 +170,7 @@ export class PropertyPanel implements ISelectionListener, IGModelRootListener { private lastRenderedAgentHistoryJson: string | undefined; protected isNetworkRuntime(): boolean { - return clientBehavior().networkPropertySections !== false; + return clientBehavior().networkPropertySections === true; } // ── IGModelRootListener ───────────────────────────────────────────── diff --git a/packages/diagram-client/test/client-behavior-flags.test.ts b/packages/diagram-client/test/client-behavior-flags.test.ts index 2e7d351..84bd90c 100644 --- a/packages/diagram-client/test/client-behavior-flags.test.ts +++ b/packages/diagram-client/test/client-behavior-flags.test.ts @@ -41,11 +41,11 @@ describe('clientBehavior flag consumption', () => { inject(WFPY_BEHAVIOR); const cb = clientBehavior(); // network-navigation-mouse-listener drill-down predicate - expect(cb.graphSourceNavigation !== false).toBe(false); + expect(cb.graphSourceNavigation === true).toBe(false); // property-panel isNetworkRuntime predicate - expect(cb.networkPropertySections !== false).toBe(false); + expect(cb.networkPropertySections === true).toBe(false); // navigation-ui entity-label predicate - expect(cb.networkNavigationLabels !== false).toBe(false); + expect(cb.networkNavigationLabels === true).toBe(false); // property-panel CLI-tools "none" sentinel expect(cb.noneSentinel).toBe('wfpy-none'); // views.ts icon detection @@ -57,19 +57,19 @@ describe('clientBehavior flag consumption', () => { it('resolves the calpy truth table (network behaviors on)', () => { inject(CALPY_BEHAVIOR); const cb = clientBehavior(); - expect(cb.graphSourceNavigation !== false).toBe(true); - expect(cb.networkPropertySections !== false).toBe(true); - expect(cb.networkNavigationLabels !== false).toBe(true); + expect(cb.graphSourceNavigation === true).toBe(true); + expect(cb.networkPropertySections === true).toBe(true); + expect(cb.networkNavigationLabels === true).toBe(true); expect(cb.noneSentinel).toBe('wfpy-none'); expect((cb.scriptInterpreterCommands ?? []).includes('python')).toBe(true); expect((cb.scriptInterpreterCommands ?? []).includes('python3')).toBe(true); }); - it('reads the network behaviours as on, with no sentinel, when nothing is injected (a product that sets none, dev/standalone)', () => { + it('reads as all-off with no sentinel when nothing is injected (dev/standalone)', () => { const cb = clientBehavior(); - expect(cb.graphSourceNavigation !== false).toBe(true); - expect(cb.networkPropertySections !== false).toBe(true); - expect(cb.networkNavigationLabels !== false).toBe(true); + expect(cb.graphSourceNavigation === true).toBe(false); + expect(cb.networkPropertySections === true).toBe(false); + expect(cb.networkNavigationLabels === true).toBe(false); expect(cb.noneSentinel).toBeUndefined(); expect((cb.scriptInterpreterCommands ?? []).includes('python')).toBe(false); }); diff --git a/packages/extension-core/src/api.ts b/packages/extension-core/src/api.ts index f2ad051..fe11689 100644 --- a/packages/extension-core/src/api.ts +++ b/packages/extension-core/src/api.ts @@ -115,11 +115,11 @@ export interface DiagramStorageOptions { } /** - * Chat carry-over: everything the diagram chat backend + intent resolver need - * that is not already on the profile root. `operationPrefix` is the internal - * namespace the chat's operation dispatcher routes under; `nodeCommands` are the - * runtime's node-creation slash commands; `skill` is the domain primer injected - * into each session's context. + * Chat carry-over: everything the diagram chat backend needs that is not + * already on the profile root. `nodeCommands` are the runtime's node-creation + * slash commands (none when absent); `skill` is the domain primer injected into + * each session's context. The operation prefix the edits route under is the + * storage options', not this block's. * * REALM NOTE: the function-valued fields (graph/turn/selection context, tool * handlers, slash-command handlers) follow the {@link DiagramProfile.serverDiagramModule} @@ -357,14 +357,11 @@ export interface DiagramClientBehavior { /** The ACP connectors the property panel offers on an agent's `connector`; * listed per document by the platform from {@link DiagramChatConfig.acpConnector}. */ acpConnectors?: AcpConnectorInfo[]; - /** Cross-file drill-down navigation resolves through the graph source model. - * On by default; a product whose navigation runs on the file's text sets `false`. */ + /** Cross-file drill-down navigation resolves through the graph source model. */ graphSourceNavigation?: boolean; - /** Property panel renders the network-model sections and labels. - * On by default; a product whose model is a workflow sets `false`. */ + /** Property panel renders the network-model sections and labels. */ networkPropertySections?: boolean; - /** Cross-file navigation UI uses the network entity vocabulary. - * On by default; a product whose model is a workflow sets `false`. */ + /** Cross-file navigation UI uses the network entity vocabulary. */ networkNavigationLabels?: boolean; /** Sentinel option value/default for the agent CLI-tools "none" selection. */ noneSentinel?: string; diff --git a/packages/extension-core/test/edit-capability.test.ts b/packages/extension-core/test/edit-capability.test.ts index 84083f7..9068597 100644 --- a/packages/extension-core/test/edit-capability.test.ts +++ b/packages/extension-core/test/edit-capability.test.ts @@ -37,14 +37,14 @@ function makeProfile() { } as any; } -function makeCapability(backend = makeBackend()) { +function makeCapability(backend = makeBackend(), profile = makeProfile()) { const refresh = vi.fn(); const editorProvider = { refreshModelFromDisk: refresh, getRefreshContext: vi.fn(() => ({ networkName: 'net1' })) }; const capability = createEditChatCapability({ - profile: makeProfile(), + profile, editBackend: backend, getEditorProvider: () => editorProvider as any, log: () => undefined @@ -64,6 +64,15 @@ beforeEach(() => { }); describe('slash contributions', () => { + it('offers no node-creation command when the profile names none', () => { + const profile = makeProfile(); + profile.chat = { fullName: 'Workflow Chat' }; + const { capability } = makeCapability(makeBackend(), profile); + const names = capability.slashCommands.map(c => c.command); + expect(names.filter(n => n.startsWith('create-'))).toEqual([]); + expect(names).toEqual(expect.arrayContaining(['connect', 'delete', 'rename', 'layout'])); + }); + it('contributes the profile\'s node commands, edit commands and /layout', () => { const { capability } = makeCapability(); const names = capability.slashCommands.map(c => c.command); diff --git a/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts b/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts index 81a9775..7fada21 100644 --- a/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts +++ b/packages/sidecar-toolkit/src/sidecar-diagram-profile.ts @@ -396,9 +396,9 @@ export type ResolvedSidecarProfileInput = export function resolveSidecarCommandIds( input: Pick ): SidecarProfileCommandIds { - const client = input.glspClientName ?? input.key; + const client = nonBlank(input.glspClientName) ?? input.key; const glsp = (field: string): string => `${client}.glsp.${field}`; - const given = input.commands ?? {}; + const given: Partial> = input.commands ?? {}; const derived: SidecarProfileCommandIds = { openDiagram: glsp('openDiagram'), openDiagramSplit: glsp('openDiagramSplit'), @@ -421,15 +421,20 @@ export function resolveSidecarCommandIds( sidecarSend: `${input.sidecarOperationPrefix}.sidecar.send`, createNewContainer: glsp('createNewContainer') }; - const resolved: Record = { ...derived }; - for (const [field, id] of Object.entries(given)) { - if (typeof id === 'string' && id.trim() !== '') { - resolved[field] = id; - } + // Only the ids the platform registers come through: a deprecated id a + // product still spells, or a key it made up, stays out of the profile. + const resolved: Record = {}; + for (const [field, derivedId] of Object.entries(derived)) { + resolved[field] = nonBlank(given[field as keyof SidecarProfileCommandIds]) ?? derivedId; } return resolved as unknown as SidecarProfileCommandIds; } +/** `value` when it says something; `undefined` for an absent or blank one, so a default applies. */ +function nonBlank(value: string | undefined): string | undefined { + return value !== undefined && value.trim() !== '' ? value : undefined; +} + /** * `input` with every field a product may leave out filled in: the command * ids, the operation kinds, the GLSP client identity, the setting keys and @@ -437,31 +442,31 @@ export function resolveSidecarCommandIds( * product gives wins. */ export function resolveSidecarProfileInput(input: SidecarProfileInput): ResolvedSidecarProfileInput { - const glspClientName = input.glspClientName ?? input.key; + const glspClientName = nonBlank(input.glspClientName) ?? input.key; const prefix = input.sidecarOperationPrefix; return { ...input, glspClientName, - glspClientId: input.glspClientId ?? `glsp.${glspClientName}`, + glspClientId: nonBlank(input.glspClientId) ?? `glsp.${glspClientName}`, commands: resolveSidecarCommandIds(input), operationKinds: input.operationKinds ?? { createEntityPort: `${prefix}.createEntityPort`, deleteEntityPort: `${prefix}.deleteEntityPort` }, - sidecarCommandSettingKey: input.sidecarCommandSettingKey + sidecarCommandSettingKey: nonBlank(input.sidecarCommandSettingKey) ?? (input.sidecarCommandDefault !== undefined ? 'sidecarCommand' : undefined), - cliCommandSettingKey: input.cliCommandSettingKey ?? 'cliCommand', + cliCommandSettingKey: nonBlank(input.cliCommandSettingKey) ?? 'cliCommand', acceptedOperationPrefixes: input.acceptedOperationPrefixes ?? [prefix], - undoLabelSuffix: input.undoLabelSuffix ?? ` (${input.key})`, - scopeArgKey: input.scopeArgKey ?? 'workflow', - runOutputDirSettingKey: input.runOutputDirSettingKey ?? 'runOutputDir', - liveExecutionGlowSettingKey: input.liveExecutionGlowSettingKey ?? 'liveExecutionGlow', - agentToolsSettingKey: input.agentToolsSettingKey ?? 'agentTools', - agentToolAuthSettingKey: input.agentToolAuthSettingKey ?? 'agentToolAuth', - agentToolPolicySettingKey: input.agentToolPolicySettingKey ?? 'agentToolPolicy', - agentToolTimeoutMsSettingKey: input.agentToolTimeoutMsSettingKey ?? 'agentToolTimeoutMs', - agentToolRegistrySettingKey: input.agentToolRegistrySettingKey ?? 'agentToolRegistry', - agentMcpBridgeCmdSettingKey: input.agentMcpBridgeCmdSettingKey ?? 'agentMcpBridgeCmd' + undoLabelSuffix: nonBlank(input.undoLabelSuffix) ?? ` (${input.key})`, + scopeArgKey: nonBlank(input.scopeArgKey) ?? 'workflow', + runOutputDirSettingKey: nonBlank(input.runOutputDirSettingKey) ?? 'runOutputDir', + liveExecutionGlowSettingKey: nonBlank(input.liveExecutionGlowSettingKey) ?? 'liveExecutionGlow', + agentToolsSettingKey: nonBlank(input.agentToolsSettingKey) ?? 'agentTools', + agentToolAuthSettingKey: nonBlank(input.agentToolAuthSettingKey) ?? 'agentToolAuth', + agentToolPolicySettingKey: nonBlank(input.agentToolPolicySettingKey) ?? 'agentToolPolicy', + agentToolTimeoutMsSettingKey: nonBlank(input.agentToolTimeoutMsSettingKey) ?? 'agentToolTimeoutMs', + agentToolRegistrySettingKey: nonBlank(input.agentToolRegistrySettingKey) ?? 'agentToolRegistry', + agentMcpBridgeCmdSettingKey: nonBlank(input.agentMcpBridgeCmdSettingKey) ?? 'agentMcpBridgeCmd' }; } diff --git a/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts b/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts index b2f9588..c6c9a62 100644 --- a/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts +++ b/packages/sidecar-toolkit/test/derived-profile-inputs.test.ts @@ -75,6 +75,23 @@ describe('resolveSidecarCommandIds', () => { expect(ids.createNewContainer).toBe('sv.glsp.createNewModule'); expect(ids.undo).toBe('sv.glsp.undo'); }); + + it('leaves a deprecated id a product still spells out of the profile', () => { + const ids = resolveSidecarCommandIds({ + key: 'sv', + sidecarOperationPrefix: 'sv', + commands: { createAgentToolPolicyFile: 'sv.glsp.createAgentToolPolicyFile', chatAddViewerEditor: 'x', chatAddViewerTask: 'y' } + }); + expect(ids).not.toHaveProperty('createAgentToolPolicyFile'); + expect(ids).not.toHaveProperty('chatAddViewerEditor'); + expect(ids).not.toHaveProperty('chatAddViewerTask'); + }); + + it('derives from the key when the GLSP client name is blank', () => { + const ids = resolveSidecarCommandIds({ key: 'sv', glspClientName: ' ', sidecarOperationPrefix: 'sv' }); + expect(ids.openDiagram).toBe('sv.glsp.openDiagram'); + expect(resolveSidecarProfileInput(minimalInput({ glspClientName: '', glspClientId: ' ' })).glspClientId).toBe('glsp.sv'); + }); }); describe('resolveSidecarProfileInput', () => { From 568afdf31c65ffdaf88b39b9882a9b57c494de35 Mon Sep 17 00:00:00 2001 From: Endri Bezati Date: Sun, 8 Nov 2026 09:42:55 +0100 Subject: [PATCH 3/5] refactor: one declaration of the behaviour flags, the command ids and the chat tool Phase 4c of the declutter plan. DiagramClientBehavior (with AcpConnectorInfo), DiagramCommandIds and InProcessChatTool were each declared by hand in two or three packages: the API, the toolkit (as SidecarClientBehavior, SidecarProfileCommandIds, RegistryChatTool) and the webview, with a test to keep two of the twins in step. They live in @dialogram/shared now, the API re-exports them, the toolkit's names are the shared types (ProductClientBehavior is the shared behaviour minus the four fields the platform derives), and the webview reads them. The twin test goes with the second twin. scriptInterpreterCommands is deprecated: nothing reads it. Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv --- CHANGELOG.md | 10 + packages/diagram-client/src/profile.ts | 83 +------- packages/extension-core/src/api.ts | 186 ++---------------- packages/shared/src/chat-tools.ts | 24 +++ packages/shared/src/client-behavior.ts | 125 ++++++++++++ packages/shared/src/command-ids.ts | 31 +++ packages/shared/src/index.ts | 3 + .../sidecar-toolkit/src/registry-tools.ts | 22 +-- .../src/sidecar-diagram-profile.ts | 68 +------ .../test/client-behavior-twin.test.ts | 98 --------- 10 files changed, 218 insertions(+), 432 deletions(-) create mode 100644 packages/shared/src/chat-tools.ts create mode 100644 packages/shared/src/client-behavior.ts create mode 100644 packages/shared/src/command-ids.ts delete mode 100644 packages/sidecar-toolkit/test/client-behavior-twin.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a04564..1abcd91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,16 @@ platform API version. ### Changed +- **One declaration of the webview's behaviour flags, the command ids and + the in-process chat tool.** `DiagramClientBehavior` (with + `AcpConnectorInfo`), `DiagramCommandIds` and `InProcessChatTool` live in + `@dialogram/shared`; the API re-exports them, the toolkit's + `SidecarClientBehavior`, `SidecarProfileCommandIds` and `RegistryChatTool` + are the shared types (the product-facing behaviour is + `ProductClientBehavior`, the shared type minus the four fields the platform + derives), and the webview reads them instead of its own copy. The test that + kept two hand-written twins in step is gone with the second twin. + `scriptInterpreterCommands` is deprecated: nothing reads it. - **A sidecar profile names what is its own; the toolkit derives the rest.** `SidecarProfileInput` no longer requires the command ids, the operation kinds, the GLSP client identity, the CLI and sidecar setting keys, the diff --git a/packages/diagram-client/src/profile.ts b/packages/diagram-client/src/profile.ts index 7e242f4..06b4e46 100644 --- a/packages/diagram-client/src/profile.ts +++ b/packages/diagram-client/src/profile.ts @@ -1,94 +1,21 @@ -import type { NodeFamilySpec } from '@dialogram/shared'; +import type { AcpConnectorInfo, DiagramClientBehavior, DiagramCommandIds } from '@dialogram/shared'; -type DiagramCommandIds = { - fitToScreen?: string; - runWorkflow?: string; - stopWorkflow?: string; - setQueueTraceVisible?: string; - setAgentToolConfig?: string; - getAgentToolConfig?: string; -}; +export type { AcpConnectorInfo, DiagramClientBehavior }; type DiagramOperationKinds = { createEntityPort?: string; deleteEntityPort?: string; }; -type DiagramClientBehavior = { - graphSourceNavigation?: boolean; - networkPropertySections?: boolean; - networkNavigationLabels?: boolean; - noneSentinel?: string; - scriptInterpreterCommands?: string[]; - paletteIcons?: Record; - nodeFamilies?: NodeFamilySpec[]; - /** Whether the host has a chat backend; derived by the platform. */ - chatBackend?: boolean; - /** The ACP connectors the runtime discovered or the user declared, for an - * agent's `connector` (extension-core `AcpConnectorInfo`). */ - acpConnectors?: AcpConnectorInfo[]; - /** Whether a run can be resumed at a step of its queue trace. */ - resumeAtStep?: boolean; - /** Where a nested workflow defined in another file opens: `in-place` shows it in - * this editor, as a view of the editor's root (one editor per hierarchy); - * `new-editor` (the default) opens the file in its own editor. */ - nestedNavigation?: 'in-place' | 'new-editor'; - /** Whether a double-click on a node that is not a network instance opens the - * source it references in the text editor. Off (the default), such a node - * does nothing on double-click. For a product whose leaves have a body of - * their own that no diagram shows yet: an HDL module or entity of processes - * and assignments lands at its definition. */ - doubleClickOpensSource?: boolean; - /** Whether anything is written on a wire: the carried label (a width, a - * payload type), the queue badge and last-token button of a run, the - * index labels of a list connection. Default true. Off, an edge is its - * line, and the tooltip on hover still says what it carries. 'once': as - * true, but of the edges that leave or reach one port together with the - * same carried label, only the first writes it; the others say it in - * their tooltip (a node with many edges of one type, all on one trunk). */ - edgeCaptions?: boolean | 'once'; - /** Property-panel sections a product does without, each `false` to hide - * it (all shown by default): `ports`, the input and output port lists, for - * a product whose pins carry no meaning; `annotations`, the raw - * definition annotations, for one whose facts already say it all; - * `parameters`, the definition's parameters, for one whose nodes have - * none (its own editable fields still show); `entityType`, Basic Info's - * Type row (task, workflow) and its change-type button, for one whose - * node kinds are its own. */ - propertySections?: { ports?: boolean; annotations?: boolean; parameters?: boolean; entityType?: boolean }; - /** The profile's status source, as the context menu offers it: "Show - *