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
28 changes: 20 additions & 8 deletions docs/proposals/single-editor-hierarchy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Proposal: one editor for a whole workflow hierarchy

**Status:** proposal. Nothing here is implemented.
**Status:** phases 1 and 2 are implemented (in-place navigation, the root's run, and the view in the chat's context). The GLSP-MCP root/view split, phases 3 to 5 and the outline are not.
**Affects:** dialogram (most of it), wfpy (a hierarchy export), wfpy-ide (one
profile flag).

Expand Down Expand Up @@ -197,13 +197,25 @@ comes from the same export, and is the cheap part of it: names, files and sizes.

## Phasing

1. **In-place navigation.** The profile flag, drill-down / breadcrumb / "Used
By" in place, the host's per-editor trail and refresh context, and "Open in
its own editor".
2. **The root runs, the chat knows the view.** Root fields in the diagram
context, and Run, Rerun from Here and ⟲ from the root. The run driver and
refresh keyed by the editor. The trail in the chat's turn context, and the
GLSP-MCP root/view split.
1. **In-place navigation.** *Done.* `clientBehavior.nestedNavigation`
(`'in-place'` / `'new-editor'`, default `'new-editor'`). Drill-down and the
breadcrumb stay in the editor for any file. "Used By" navigates in place to
an ancestor in the trail or to a workflow of the editor's own document, and
opens a caller in another file as a new root with no trail. The client's
navigation stack is keyed by the editor's root. The host keys the refresh
context by the editor (from the request's client) and remembers the shown
file, so a save, an external change or a live preview refreshes the view
shown. For another file it reloads from disk, since live-preview content is
the root's text. A node menu item opens a nested workflow's file in its own
editor.
2. **The root runs, the chat knows the view.** *Done, except the GLSP-MCP
split.* The diagram context carries `rootSourceUri`, `rootWorkflowName` and
the trail. ▶ Run, ⟲ and "Rerun from Here" use the root, and inside a nested
view "Rerun from Here" reruns the root-level instance the view is in. The
refresh during and after a run reloads the editor's current view (file,
workflow, trail) rather than jumping to the root. The chat panel sends the
view's trail with the selection and each message, and the turn context names
the nested view and the file defining it.
3. **Editing nested views.** Secondary documents saved after undo/redo, the root
editor watching every file in its trail, and the concurrency behaviour
tested.
Expand Down
17 changes: 16 additions & 1 deletion packages/diagram-client/src/chat-panel-integrated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,20 @@ interface CommandEntry {
* Communication with the extension host happens over the GLSP vscode-messenger
* channel (the diagram webview's raw postMessage is owned by GLSP).
*/

/**
* The view on screen, as a trail from the editor's root, when it is not the
* root itself.
*
* The chat is the root's for the whole hierarchy, so without this the agent
* cannot tell that the diagram shows a nested workflow -- nor which instance
* the selected nodes belong to.
*/
export function currentViewTrail(): Array<{ sourceUri: string; workflowName: string; workflowInstanceName?: string }> | undefined {
const trail = (globalThis as any).__calDiagramContext?.trail;
return Array.isArray(trail) && trail.length > 1 ? trail : undefined;
}

@injectable()
export class ChatPanel implements IDiagramStartup, ISelectionListener {
/** Run after the diagram model is ready so the webview DOM exists. */
Expand Down Expand Up @@ -258,7 +272,7 @@ export class ChatPanel implements IDiagramStartup, ISelectionListener {
*/
selectionChanged(_root: unknown, selectedElements: string[]): void {
this.selectedNodeIds = selectedElements ?? [];
this.sendToHost('chat.selection', { selectedNodeIds: this.selectedNodeIds });
this.sendToHost('chat.selection', { selectedNodeIds: this.selectedNodeIds, viewTrail: currentViewTrail() });
if (this.initialized) this.update();
}

Expand Down Expand Up @@ -811,6 +825,7 @@ export class ChatPanel implements IDiagramStartup, ISelectionListener {
sessionId: this.currentSessionId,
mode: this.currentMode,
selectedNodeIds: this.selectedNodeIds,
viewTrail: currentViewTrail(),
});

if (!ok) {
Expand Down
62 changes: 57 additions & 5 deletions packages/diagram-client/src/editing-action-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { inject, injectable, optional } from 'inversify';
import { EditorContextService, IActionDispatcher, IGridManager, TYPES } from '@eclipse-glsp/client';
import { Action as GlspAction, ApplyLabelEditOperation, ICommand, IActionHandler } from '@eclipse-glsp/sprotty';
import { Action as GlspAction, ApplyLabelEditOperation, ICommand, IActionHandler, NavigateToExternalTargetAction } from '@eclipse-glsp/sprotty';
import { VscodeUi, type VscodeQuickPickItem } from './vscode-ui';
import { clientBehavior, commandId, settingsNamespace } from './profile';
import { rerunActorFor } from './navigation-ui';
import { NETWORK_NAME_ARG, OPEN_DIAGRAM_ARG } from './network-navigation-target';
import { EXECUTION_OVERLAY_ACTION_KIND, type ExecutionOverlayActionPayload } from '@dialogram/shared';

export type WorkflowWorkspaceEntity = {
Expand Down Expand Up @@ -87,6 +89,21 @@ export namespace WorkflowRerunFromHereAction {
}
}

export namespace WorkflowOpenInOwnEditorAction {
export const KIND = 'dialogram.openInOwnEditor';

export interface Action extends GlspAction {
kind: typeof KIND;
uri: string;
workflowName: string;
}

export function is(action: unknown): action is Action {
return !!action && typeof action === 'object' && (action as any).kind === KIND
&& typeof (action as any).uri === 'string' && typeof (action as any).workflowName === 'string';
}
}

export namespace WorkflowRenameEntityOperation {
export const KIND = 'dialogram.renameEntity';

Expand Down Expand Up @@ -1198,19 +1215,54 @@ export class WorkflowRerunFromHereActionHandler implements IActionHandler {
return;
}
const context = (globalThis as any).__calDiagramContext as
| { sourceUri?: string; workflowName?: string; runDir?: string }
| {
sourceUri?: string;
workflowName?: string;
runDir?: string;
rootSourceUri?: string;
rootWorkflowName?: string;
trail?: Array<{ workflowInstanceName?: string }>;
}
| undefined;
// The menu offers it only for a run that can be resumed; anything the
// run driver then finds wrong with it, the driver reports.
if (!context?.sourceUri || !context.runDir || !clientBehavior().resumeAtStep) {
console.warn('[workflow] Rerun from Here: no resumable run is shown');
return;
}
// The root's run, at the root's step: inside a nested view, the node is
// rerun by rerunning the instance it is in.
void VscodeUi.instance.executeCommand(commandId('runWorkflow'), [{
sourceUri: context.sourceUri,
workflowName: context.workflowName,
sourceUri: context.rootSourceUri ?? context.sourceUri,
workflowName: context.rootWorkflowName ?? context.workflowName,
resumeFrom: context.runDir,
resumeAtActor: action.entityName
resumeAtActor: rerunActorFor(action.entityName, context.trail ?? [])
}]);
}
}


/**
* "Open in Its Own Editor": open a nested workflow's file as a root of its own.
*
* No trail goes with it, on purpose: the new editor's root is that file, with
* its own chat and runs, not a view of this editor's hierarchy.
*/
@injectable()
export class WorkflowOpenInOwnEditorActionHandler implements IActionHandler {
@inject(TYPES.IActionDispatcher)
protected readonly actionDispatcher!: IActionDispatcher;

handle(action: GlspAction): ICommand | GlspAction | void {
if (!WorkflowOpenInOwnEditorAction.is(action)) {
return;
}
void this.actionDispatcher.dispatch(NavigateToExternalTargetAction.create({
uri: action.uri,
args: {
[OPEN_DIAGRAM_ARG]: true,
[NETWORK_NAME_ARG]: action.workflowName
}
}) as never);
}
}
Loading
Loading