Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ platform API version.

## [Unreleased]

**Release:** this release must bump `DIALOGRAM_API_VERSION` to 0.9 and ship
with products built against it: its API removals (#127, #129) and the stock
bundle's dropping the agent workflow UI (below) need a product still at 0.8
to fail its version check rather than run against what it no longer has.

### Removed

- **The `@path` annotation editor and its wizard template.** No producer has
Expand Down Expand Up @@ -96,6 +101,19 @@ platform API version.

### Changed

- **Breaking for a product that showed the agent workflow UI from the stock
bundle: it no longer loads it.** The `@tool` and `@agent` annotation
editors, the agent sections of a node's panel (Skill row, Agent Tools, live
Agent Context), the wizard's `@tool` and `@viewer` templates and the
handlers of the server's skill and agent-preset answers were wfpy's, yet
every stock-bundle product showed the editors, the header's "Add @tool" and
"Add @agent" and the templates. They are `agentUiModule`, which wfpy-ide's
own webview bundle loads (endrix/wfpy-ide#24, merged). sv, vhdl, ndf and
calpy get the plain annotation card and only the wizard's "Custom
annotation…"; none of them has `@agent` nodes, so Agent Tools, the Skill
row and Agent Context never appeared for them. The stock bundle is 141 KB
smaller. Ship in the 0.9 release (above).

- **Breaking for a profile that relied on the defaults: the Entities palette,
navigation and the source MIME type are the product's.** The tool-palette
provider no longer hard-codes wfpy's five rows (Task, Tool, Agent, Viewer,
Expand Down
18 changes: 11 additions & 7 deletions packages/diagram-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ Without `formatAnnotation`, Save writes `@name(key=value, ...)`. With
ones the card does not show. `guiOnlyArgs` are keys your editor keeps in
`args` for its own state; Save writes them nowhere. `quickAdd` puts an "add
from template" button in the Annotations header. There is one editor per
name: a later binding replaces an earlier one, so your module can replace one
of the stock editors (`@tool`, `@agent`).
name: a later binding replaces an earlier one, so a module loaded later can
replace an editor an earlier one bound.

**Node sections.** To show more about a node than the panel's own sections,
bind a `NodeSectionContribution`. It needs an `id`; every hook is optional:
Expand Down Expand Up @@ -241,9 +241,7 @@ export const myFeaturesModule = new ContainerModule(bind => {
again, for an answer that arrives later.

Contributions are asked in the order their ids were first bound. A later
binding for an id replaces the earlier one, so your module can replace the
stock bundle's agent sections (id `agent`: the Skill row, Agent Tools and the
live Agent Context).
binding for an id replaces the earlier one.

**Annotation templates.** The Annotations header's "Wizard…" adds an
annotation from a template, or from a name and arguments typed in. To offer a
Expand All @@ -267,8 +265,14 @@ export const myFeaturesModule = new ContainerModule(bind => {

There is one template per annotation name, and a later binding replaces an
earlier one. The wizard lists them before its own "Custom annotation…" entry
(`custom`), which a template cannot replace. The stock bundle binds `@tool`
and `@viewer`.
(`custom`), which a template cannot replace.

**The agent workflow UI.** The stock bundle binds no editors, sections or
templates. wfpy's are `agentUiModule`, imported by path as
`@dialogram/diagram-client/agent-ui/index`: the `@tool` and `@agent`
editors, the agent sections (id `agent`: Basic Info's Skill row, Agent Tools
and the live Agent Context) and the `@tool` and `@viewer` templates. A bundle
that wants them passes it in `features`, as wfpy-ide's does.

**A diagram of your own.** When you compose the container yourself:

Expand Down
50 changes: 36 additions & 14 deletions packages/diagram-client/src/agent-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,71 @@
* The agent workflow UI: the `@tool` and `@agent` annotation editors, the agent
* sections of a node's panel, and the wizard's `@tool` and `@viewer` templates.
*
* `agentUiModule` binds all of it. A product's own webview bundle passes it to
* `startStockDiagram({ features: [agentUiModule] })` (imported by path,
* `@dialogram/diagram-client/agent-ui/index`); the stock feature module binds
* the same today, and each contribution point keeps the last binding per
* name or id, so binding it twice draws everything once.
* `agentUiModule` binds all of it. The stock bundle does not load it: a
* product's own webview bundle passes it to
* `startStockDiagram({ features: [agentUiModule] })`, imported by path as
* `@dialogram/diagram-client/agent-ui/index`.
*/
import { ContainerModule, type interfaces } from 'inversify';
import { configureActionHandler } from '@eclipse-glsp/client';
import { AnnotationEditor } from '../annotation-editor';
import { AnnotationTemplate } from '../annotation-template';
import { NodeSectionContribution } from '../node-section';
import {
WorkflowShowAgentSkillsAction,
WorkflowShowAgentSkillsActionHandler,
WorkflowShowClaudeAgentsAction,
WorkflowShowClaudeAgentsActionHandler
} from '../editing-action-handlers';
import { AgentAnnotationEditor } from './agent-editor';
import { AgentNodeSections } from './agent-sections';
import { ToolAnnotationEditor } from './tool-editor';
import { ToolAnnotationTemplate } from './tool-template';
import { ViewerAnnotationTemplate } from './viewer-template';

/** Fresh instances, in the order the panel shows them. */
export function stockAnnotationEditors(): AnnotationEditor[] {
export function agentUiEditors(): AnnotationEditor[] {
return [new ToolAnnotationEditor(), new AgentAnnotationEditor()];
}

/** A fresh instance: it keeps the chat history it last drew. */
export function stockNodeSections(): NodeSectionContribution[] {
export function agentUiNodeSections(): NodeSectionContribution[] {
return [new AgentNodeSections()];
}

/** The wizard's templates, in its menu order. */
export function stockAnnotationTemplates(): AnnotationTemplate[] {
export function agentUiTemplates(): AnnotationTemplate[] {
return [new ToolAnnotationTemplate(), new ViewerAnnotationTemplate()];
}

/** Bind fresh instances of all of it into a feature module. */
export function bindAgentUi(bind: interfaces.Bind): void {
for (const editor of stockAnnotationEditors()) {
/** What a feature module's registry is handed, as GLSP's helpers take it. */
export interface ModuleContext {
bind: interfaces.Bind;
unbind: interfaces.Unbind;
isBound: interfaces.IsBound;
rebind: interfaces.Rebind;
}

/**
* Bind fresh instances of all of it into a feature module, and the handlers
* of the server's answers to the `@agent` editor's discovery requests (the
* skills and agent presets it found), whose caches the editor and the Skill
* row read.
*/
export function bindAgentUi(context: ModuleContext): void {
const { bind } = context;
configureActionHandler(context, WorkflowShowAgentSkillsAction.KIND, WorkflowShowAgentSkillsActionHandler);
configureActionHandler(context, WorkflowShowClaudeAgentsAction.KIND, WorkflowShowClaudeAgentsActionHandler);
for (const editor of agentUiEditors()) {
bind(AnnotationEditor).toConstantValue(editor);
}
for (const sections of stockNodeSections()) {
for (const sections of agentUiNodeSections()) {
bind(NodeSectionContribution).toConstantValue(sections);
}
for (const template of stockAnnotationTemplates()) {
for (const template of agentUiTemplates()) {
bind(AnnotationTemplate).toConstantValue(template);
}
}

/** The agent workflow UI as a feature module of its own. */
export const agentUiModule = new ContainerModule(bind => bindAgentUi(bind));
export const agentUiModule = new ContainerModule((bind, unbind, isBound, rebind) => bindAgentUi({ bind, unbind, isBound, rebind }));
4 changes: 2 additions & 2 deletions packages/diagram-client/src/annotation-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
*
* and passes the module to `startStockDiagram({ features })`. There is one
* editor per annotation name: a later binding for a name replaces the earlier
* one, so a product's module, which loads after the stock one, can replace a
* stock editor. The section header's quick-add buttons follow the order in
* one, so a module that loads later can replace an editor an earlier one
* bound (the stock diagram binds none). The section header's quick-add buttons follow the order in
* which the names were first bound.
*/
import type { IActionDispatcher } from '@eclipse-glsp/client';
Expand Down
4 changes: 2 additions & 2 deletions packages/diagram-client/src/annotation-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
*
* and passes the module to `startStockDiagram({ features })`. There is one
* template per annotation name: a later binding for a name replaces the
* earlier one, so a product's module, which loads after the stock one, can
* replace a stock template. The wizard lists them in the order the names were
* earlier one, so a module that loads later can replace a template an
* earlier one bound (the stock diagram binds none). The wizard lists them in the order the names were
* first bound, before its own "Custom annotation…" entry; a template without a
* name is ignored.
*/
Expand Down
6 changes: 3 additions & 3 deletions packages/diagram-client/src/contributions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* How a contribution point picks among what products bound: one per key, a
* later binding replacing an earlier one in the earlier one's place. A
* product's feature module loads after the stock one, so it can replace a
* stock contribution. An empty key drops the contribution.
* later binding replacing an earlier one in the earlier one's place, so a
* module that loads later can replace what an earlier one bound. An empty
* key drops the contribution.
*/
export function lastPerKey<T>(bound: readonly T[] | undefined, keyOf: (item: T) => string): T[] {
const byKey = new Map<string, T>();
Expand Down
6 changes: 3 additions & 3 deletions packages/diagram-client/src/node-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
*
* and passes the module to `startStockDiagram({ features })`. There is one
* contribution per `id`: a later binding for an id replaces the earlier one, so
* a product's module, which loads after the stock one, can replace a stock
* contribution. Contributions are asked in the order their ids were first
* a module that loads later can replace a contribution an earlier one bound
* (the stock diagram binds none). Contributions are asked in the order their ids were first
* bound, so their rows and sections come in that order.
*/
import type { PropertyElement } from './property-model';
Expand Down Expand Up @@ -47,7 +47,7 @@ export interface NodeSectionHost {
}

export interface NodeSectionContribution {
/** What the contribution is, for a later binding to replace it: the stock one is `'agent'`. */
/** What the contribution is, for a later binding to replace it (the agent sections are `'agent'`). */
readonly id: string;

/** Once, when the panel starts. */
Expand Down
4 changes: 2 additions & 2 deletions packages/diagram-client/src/property-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ export class PropertyPanel implements ISelectionListener, IGModelRootListener {

/**
* One editor per annotation name, in the order the names were first bound.
* A later binding for a name replaces the earlier one, so a product's
* module, loaded after the stock one, can replace a stock editor.
* A later binding for a name replaces the earlier one, so a module that
* loads later can replace an editor an earlier one bound.
*/
protected annotationEditors(): readonly AnnotationEditor[] {
return lastPerKey(this.contributedAnnotationEditors, editor => editor.annotationName);
Expand Down
22 changes: 7 additions & 15 deletions packages/diagram-client/src/stock-features.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { ContainerModule } from 'inversify';
import { ChangeBoundsTool, TYPES, bindAsService, configureActionHandler } from '@eclipse-glsp/client';
import { MoveAction } from '@eclipse-glsp/sprotty';
import { PropertyPanel } from './property-panel';
import { bindAgentUi } from './agent-ui';
import { ChatPanel } from './chat-panel-integrated';
import { clientBehavior } from './profile';
import { WorkflowNavigationUi } from './navigation-ui';
Expand Down Expand Up @@ -49,11 +48,7 @@ import {
WorkflowEditParametersAction,
WorkflowEditParametersActionHandler,
WorkflowEditAnnotationsAction,
WorkflowEditAnnotationsActionHandler,
WorkflowShowAgentSkillsAction,
WorkflowShowAgentSkillsActionHandler,
WorkflowShowClaudeAgentsAction,
WorkflowShowClaudeAgentsActionHandler
WorkflowEditAnnotationsActionHandler
} from './editing-action-handlers';

/**
Expand All @@ -67,16 +62,15 @@ export const workflowFeaturesModule = new ContainerModule((bind, unbind, isBound
bind(WorkflowNavigationUi).toSelf().inSingletonScope();

// Register property panel as both selection listener and model-root listener.
// IGModelRootListener fires on every SetModelAction, enabling the panel to
// update agent context in real time even when the selection hasn't changed.
// IGModelRootListener fires on every SetModelAction, so a node section can
// update itself in place while a run streams, even when the selection
// hasn't changed (`NodeSectionContribution.modelChanged`).
bind(PropertyPanel).toSelf().inSingletonScope();
bind(TYPES.ISelectionListener).toService(PropertyPanel);
bind(TYPES.IGModelRootListener).toService(PropertyPanel);
// The agent workflow UI (agent-ui/): the panel's `@tool` and `@agent`
// editors, the agent sections of a node's panel (Basic Info's Skill row,
// Agent Tools, the live Agent Context) and the wizard's `@tool` and
// `@viewer` templates. A product's module binds its own the same way.
bindAgentUi(bind);
// No annotation editors, node sections or wizard templates of its own: a
// product binds those in its feature module. The agent workflow UI is
// `agentUiModule` (agent-ui/), which a product's own bundle loads.

// The hierarchy outline, fed by the root model's `wf:hierarchy`. Shown only
// when the product navigates nested workflows in place and the model
Expand Down Expand Up @@ -136,8 +130,6 @@ export const workflowFeaturesModule = new ContainerModule((bind, unbind, isBound
configureActionHandler(context, WorkflowOpenViewAction.KIND, WorkflowOpenViewActionHandler);
configureActionHandler(context, WorkflowEditParametersAction.KIND, WorkflowEditParametersActionHandler);
configureActionHandler(context, WorkflowEditAnnotationsAction.KIND, WorkflowEditAnnotationsActionHandler);
configureActionHandler(context, WorkflowShowAgentSkillsAction.KIND, WorkflowShowAgentSkillsActionHandler);
configureActionHandler(context, WorkflowShowClaudeAgentsAction.KIND, WorkflowShowClaudeAgentsActionHandler);

// The profile's status source on the canvas's context menu ("Show <label>",
// "Clear <label>"), merged with the server's items; nothing without one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { WorkflowEditAnnotationsActionHandler } from '../../src/editing-action-handlers';
import { stockAnnotationTemplates } from '../../src/agent-ui';
import { agentUiTemplates } from '../../src/agent-ui';
import { ui, installPage, taskNode, BASH_TOOL } from './harness';
import { resetAgentUi, rootOf } from './panel';

Expand All @@ -16,8 +16,8 @@ class TestWizard extends WorkflowEditAnnotationsActionHandler {
readonly dispatched: any[] = [];
constructor(node: any) {
super();
// What the stock feature module binds.
(this as any).contributedTemplates = stockAnnotationTemplates();
// What `agentUiModule` binds.
(this as any).contributedTemplates = agentUiTemplates();
(this as any).editorContext = { modelRoot: rootOf(node) };
(this as any).actionDispatcher = { dispatch: async (a: unknown) => { this.dispatched.push(a); } };
}
Expand Down
8 changes: 4 additions & 4 deletions packages/diagram-client/test/agent-ui/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* runs on demand: see `listenForDiscovery`.
*/
import { PropertyPanel } from '../../src/property-panel';
import { stockAnnotationEditors, stockNodeSections } from '../../src/agent-ui';
import { agentUiEditors, agentUiNodeSections } from '../../src/agent-ui';
import { WorkflowShowAgentSkillsActionHandler, WorkflowShowClaudeAgentsActionHandler } from '../../src/editing-action-handlers';
import { ui } from './harness';

Expand All @@ -35,9 +35,9 @@ export class TestPanel extends PropertyPanel {

constructor() {
super();
// What the stock feature module binds.
(this as any).contributedAnnotationEditors = stockAnnotationEditors();
(this as any).contributedNodeSections = stockNodeSections();
// What `agentUiModule` binds.
(this as any).contributedAnnotationEditors = agentUiEditors();
(this as any).contributedNodeSections = agentUiNodeSections();
(this as any).actionDispatcher = {
dispatch: async (action: unknown) => { this.dispatched.push(action); },
dispatchAll: async (actions: unknown[]) => { this.dispatched.push(...actions); }
Expand Down
18 changes: 12 additions & 6 deletions packages/diagram-client/test/annotation-editor-seam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ChangeBoundsTool, TYPES } from '@eclipse-glsp/client';
import { AnnotationEditor, type AnnotationEditorContext } from '../src/annotation-editor';
import { PropertyPanel } from '../src/property-panel';
import { workflowFeaturesModule } from '../src/stock-features.module';
import { agentUiModule } from '../src/agent-ui';
import { installPage, taskNode } from './agent-ui/harness';
import { TestPanel, button, card, resetAgentUi, rootOf, section, tick } from './agent-ui/panel';

Expand Down Expand Up @@ -179,23 +180,28 @@ describe('the editors the panel is given', () => {

const names = (panel: PropertyPanel) => (panel as any).annotationEditors().map((e: AnnotationEditor) => e.annotationName);

it('are the stock @tool and @agent editors, in that order', () => {
expect(names(stockContainer().get(PropertyPanel))).toEqual(['tool', 'agent']);
it('are none in the stock diagram, and only a product’s when it binds one', () => {
const product = new ContainerModule(bind => {
bind(AnnotationEditor).toConstantValue(probeEditor());
});
expect(names(stockContainer().get(PropertyPanel))).toEqual([]);
expect(names(stockContainer(product).get(PropertyPanel))).toEqual(['probe']);
});

it('include a product’s, after the stock ones', () => {
it('are the agent UI’s @tool and @agent when a bundle loads it, then another module’s', () => {
const product = new ContainerModule(bind => {
bind(AnnotationEditor).toConstantValue(probeEditor());
});
expect(names(stockContainer(product).get(PropertyPanel))).toEqual(['tool', 'agent', 'probe']);
expect(names(stockContainer(agentUiModule).get(PropertyPanel))).toEqual(['tool', 'agent']);
expect(names(stockContainer(agentUiModule, product).get(PropertyPanel))).toEqual(['tool', 'agent', 'probe']);
});

it('take a product’s editor in place of the stock one for its annotation', () => {
it('take a later module’s editor in place of an earlier one’s for its annotation', () => {
const mine = probeEditor([], 'agent');
const product = new ContainerModule(bind => {
bind(AnnotationEditor).toConstantValue(mine);
});
const editors = (stockContainer(product).get(PropertyPanel) as any).annotationEditors();
const editors = (stockContainer(agentUiModule, product).get(PropertyPanel) as any).annotationEditors();
expect(editors.map((e: AnnotationEditor) => e.annotationName)).toEqual(['tool', 'agent']);
expect(editors[1]).toBe(mine);
});
Expand Down
Loading
Loading