diff --git a/CHANGELOG.md b/CHANGELOG.md index d5fcc49..c04d971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,16 @@ platform API version. ### Added +- **A product whose CLI has no `run` says so: `cliRuns: false`.** The + toolkit then registers no run driver and the chat offers no resume. The + webview learns it from a derived `clientBehavior.runDriver` (like + `chatBackend`; `resumeAtStep` is now derived with it), and a diagram with no + run driver behind it shows no Run, Stop or edge toggle in the palette + header and no Agent Tools switches in the property panel, all of which + called the driver's commands. That covers ndf once it opts out, and today + chisel-ide and the library-mode mlir-viewer, whose Run invoked a command + nothing registered. sv and vhdl keep theirs: `hdl run` simulates through + it. - **A product's own webview bundle starts the stock diagram.** `startStockDiagram({ features })` (`@dialogram/diagram-client/stock-starter`) is what the stock bundle runs, with the product's feature modules loaded diff --git a/packages/diagram-client/src/profile.ts b/packages/diagram-client/src/profile.ts index 7d26ea4..af03573 100644 --- a/packages/diagram-client/src/profile.ts +++ b/packages/diagram-client/src/profile.ts @@ -78,6 +78,22 @@ export function clientBehavior(): DiagramClientBehavior { return configured && typeof configured === 'object' ? configured : {}; } +/** + * Whether the diagram offers what drives a run: Run, Stop, the toggle for what + * a run puts on the edges, and the agent-tool switches the run driver serves. + * Yes unless the host says no run driver is behind it; a webview the host did + * not describe (dev, standalone) keeps them, as before. + */ +export function offersRun(): boolean { + return clientBehavior().runDriver !== false; +} + +/** The run controls the palette header adds, by {@link offersRun}. */ +export function runHeaderControls(): { run: boolean; stop: boolean; edgeToggle: boolean } { + const runs = offersRun(); + return { run: runs, stop: runs, edgeToggle: runs }; +} + /** * The host resolves part of the behavior after the webview is up (what depends * on the machine or the workspace, e.g. the ACP connectors the runtime knows) diff --git a/packages/diagram-client/src/property-panel.ts b/packages/diagram-client/src/property-panel.ts index df46318..fe8d96f 100644 --- a/packages/diagram-client/src/property-panel.ts +++ b/packages/diagram-client/src/property-panel.ts @@ -33,7 +33,7 @@ import { VscodeUi } from './vscode-ui'; import { ppField, ppNumberField, ppReadonlyRow, renderInto } from './property-fields'; import { type PropertyElement, readNumberArg } from './property-model'; import { PropertyPanelChrome } from './property-panel-chrome'; -import { clientBehavior, commandId, operationKind } from './profile'; +import { clientBehavior, commandId, operationKind, offersRun } from './profile'; import { classifyLink, decodeURIComponentSafe, openTargetOf, referenceAction, type PanelLink } from './open-target'; import { producerFields, type ProducerField } from './producer-fields'; @@ -577,8 +577,9 @@ export class PropertyPanel implements ISelectionListener, IGModelRootListener { content.appendChild(this.buildDefinitionAnnotationsSection(node.id, args, undefined, requestAgentDiscovery)); } - // Agent Tools section (only rendered for @agent nodes) - const agentToolsSection = this.buildAgentToolsSection(entityName, args); + // Agent Tools section (only rendered for @agent nodes, and only where a run + // driver is behind the diagram: its two commands are the driver's). + const agentToolsSection = offersRun() ? this.buildAgentToolsSection(entityName, args) : null; if (agentToolsSection) { content.appendChild(agentToolsSection); } diff --git a/packages/diagram-client/src/tool-palette-header-controls.ts b/packages/diagram-client/src/tool-palette-header-controls.ts index c2524ba..8047b2e 100644 --- a/packages/diagram-client/src/tool-palette-header-controls.ts +++ b/packages/diagram-client/src/tool-palette-header-controls.ts @@ -1,7 +1,7 @@ import { codiconCSSClasses } from '@eclipse-glsp/sprotty'; import { VscodeUi } from './vscode-ui'; import { exportDiagramSvg } from './svg-export-command'; -import { commandId, feedbackEdgesVisibleStorageKey, queueTraceVisibleStorageKey } from './profile'; +import { commandId, feedbackEdgesVisibleStorageKey, queueTraceVisibleStorageKey, runHeaderControls } from './profile'; type DiagramContext = { sourceUri?: string; @@ -169,6 +169,7 @@ async function exec(command: string, args: unknown[] = []): Promise { } function patchHeaderTools(headerTools: HTMLElement): void { + const offered = runHeaderControls(); // 1) Replace reset-viewport behavior: screen-normal -> fit + center const resetIcon = headerTools.querySelector('i.codicon.codicon-screen-normal') as HTMLElement | null; if (resetIcon) { @@ -182,8 +183,8 @@ function patchHeaderTools(headerTools: HTMLElement): void { }; } - // 2) Add Run button as first header tool. - if (!headerTools.querySelector('i.codicon.codicon-play.cal-run-workflow')) { + // 2) Add Run button as first header tool, when something runs the diagram. + if (offered.run && !headerTools.querySelector('i.codicon.codicon-play.cal-run-workflow')) { const runIcon = createHeaderIcon('play'); runIcon.classList.add('cal-run-workflow'); runIcon.title = 'Run workflow'; @@ -209,8 +210,8 @@ function patchHeaderTools(headerTools: HTMLElement): void { headerTools.insertBefore(runIcon, headerTools.firstChild); } - // 3) Add Stop button to cancel an active workflow run. - if (!headerTools.querySelector('i.codicon.codicon-debug-stop.cal-stop-workflow')) { + // 3) Add Stop button to cancel an active workflow run, under the same condition. + if (offered.stop && !headerTools.querySelector('i.codicon.codicon-debug-stop.cal-stop-workflow')) { const stopIcon = createHeaderIcon('debug-stop'); stopIcon.classList.add('cal-stop-workflow'); stopIcon.title = 'Stop workflow run'; @@ -225,8 +226,8 @@ function patchHeaderTools(headerTools: HTMLElement): void { headerTools.insertBefore(stopIcon, headerTools.firstChild); } - // 4) Add queue visibility toggle icon. - if (!headerTools.querySelector('i.cal-toggle-queues')) { + // 4) Add queue visibility toggle icon: what a run puts on the edges, so only where something runs. + if (offered.edgeToggle && !headerTools.querySelector('i.cal-toggle-queues')) { const queueIcon = createHeaderIcon('eye'); queueIcon.classList.add('cal-toggle-queues'); diff --git a/packages/diagram-client/test/offers-run.test.ts b/packages/diagram-client/test/offers-run.test.ts new file mode 100644 index 0000000..9e75aa8 --- /dev/null +++ b/packages/diagram-client/test/offers-run.test.ts @@ -0,0 +1,31 @@ +/** + * What drives a run is offered unless the host says no run driver is behind + * the diagram. The palette header adds Run, Stop and the edge toggle by + * {@link runHeaderControls}; the property panel shows the agent-tool switches + * by {@link offersRun}. (The client tests run without a DOM, so the header's + * own wiring is not exercised here.) + */ +import { afterEach, describe, expect, it } from 'vitest'; +import { offersRun, runHeaderControls } from '../src/profile'; + +afterEach(() => { + delete (globalThis as any).diagramIdentifier; +}); + +describe('the run controls', () => { + it('are gone when the host says no run driver is behind the diagram', () => { + (globalThis as any).diagramIdentifier = { clientBehavior: { runDriver: false } }; + expect(offersRun()).toBe(false); + expect(runHeaderControls()).toEqual({ run: false, stop: false, edgeToggle: false }); + }); + + it('are there when one is', () => { + (globalThis as any).diagramIdentifier = { clientBehavior: { runDriver: true } }; + expect(offersRun()).toBe(true); + expect(runHeaderControls()).toEqual({ run: true, stop: true, edgeToggle: true }); + }); + + it('stay in a webview the host did not describe', () => { + expect(offersRun()).toBe(true); + }); +}); diff --git a/packages/extension-core/src/extension/diagram/diagram-editor-provider.ts b/packages/extension-core/src/extension/diagram/diagram-editor-provider.ts index 88b5352..879abae 100644 --- a/packages/extension-core/src/extension/diagram/diagram-editor-provider.ts +++ b/packages/extension-core/src/extension/diagram/diagram-editor-provider.ts @@ -990,6 +990,11 @@ export class WorkflowEditorProvider extends GlspEditorProvider { // absent is exactly what "the chat backend is not activated" // means, so it is the honest source. chatBackend: this.profile.chat !== undefined, + // The same for the run driver: Run and Stop are offered exactly + // when something is registered to run the diagram, and a run is + // resumable only where something can run it again. + runDriver: this.profile.runDriver !== undefined, + resumeAtStep: this.profile.clientBehavior?.resumeAtStep === true && this.profile.runDriver !== undefined, // The same for the status source: the menu offers exactly the // commands the host registered, or none. Data only; `collect` // stays on the host. diff --git a/packages/extension-core/test/diagram-editor-provider-client-assets.test.ts b/packages/extension-core/test/diagram-editor-provider-client-assets.test.ts index ea5467f..6717041 100644 --- a/packages/extension-core/test/diagram-editor-provider-client-assets.test.ts +++ b/packages/extension-core/test/diagram-editor-provider-client-assets.test.ts @@ -168,7 +168,7 @@ describe('WorkflowEditorProvider default webview assets (no clientAssets)', () =