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
20 changes: 17 additions & 3 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:** phases 1 to 4 are implemented (in-place navigation, the root's run, the view in the chat's context, editing nested views, and per-instance layout). Phase 5 (the hierarchy export and the outline) is not.
**Status:** implemented, all five phases: in-place navigation, the root's run and chat context, editing nested views, per-instance layout, and the hierarchy export with its cache and outline. Rows do not yet carry a run's state (see phase 5).
**Affects:** dialogram (most of it), wfpy (a hierarchy export), wfpy-ide (one
profile flag).

Expand Down Expand Up @@ -233,8 +233,22 @@ comes from the same export, and is the cheap part of it: names, files and sizes.
recomputed the key from the shown workflow's name, so a hierarchical
runtime's per-instance layouts were read per instance and saved per
workflow. A rename also moves the node in the instance's layout.
5. **The hierarchy export, the cache and the outline.** wfpy `--hierarchy`,
instance-elaborated nested graphs, the server cache, then the outline panel.
5. **The hierarchy export, the cache and the outline.** *Done.*
- wfpy: `wfpy plan --format graph --hierarchy` (huawei-csl/wfpy#45) exports
the root and every nested instance, each elaborated as its parent built
it.
- The cache: a product gives `cliHierarchyArgs`, and `CliGraphModelSource`
reads every view, root or nested, from one export per root
(`hierarchy-cache.ts`). The export is kept while every file it came from is
unchanged on disk. An instance that did not elaborate, or unsaved text
being previewed, still gets a plan of its own.
- The outline: the tree without graphs reaches the root model as
`wf:hierarchy`. The panel (`hierarchy-outline.ts`, helpers in
`hierarchy-outline-tree.ts`) has a toggle and the O key, a filter that
keeps ancestors, a single click that selects and centers the instance in
the view containing it, a double click that opens its view, and the
current row highlighted.
- Not yet: a run's state (running / done / failed, fire counts) on the rows.

Each phase is usable on its own. 1 and 2 together are what removes the
editor-per-level problem.
Expand Down
132 changes: 132 additions & 0 deletions packages/diagram-client/src/diagram-client.css
Original file line number Diff line number Diff line change
Expand Up @@ -3270,3 +3270,135 @@ details[open] > summary .agent-chat-expand-arrow {
.wf-rab-text { white-space: pre-wrap; word-break: break-word; font-size: 11.5px; line-height: 1.4; }
.wf-rab-text.stream { max-height: 160px; overflow-y: auto; }
@keyframes wf-rab-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

/* ── Hierarchy outline (hierarchy-outline.ts) ─────────────────────────── */

.workflow-hierarchy-outline-toggle {
position: fixed;
top: 44px;
left: 8px;
z-index: 30;
width: 28px;
height: 28px;
border: 1px solid var(--vscode-editorWidget-border, #454545);
border-radius: 4px;
background: var(--vscode-editorWidget-background, #252526);
color: var(--vscode-foreground, #cccccc);
cursor: pointer;
}

.workflow-hierarchy-outline-toggle.active,
.workflow-hierarchy-outline-toggle:hover {
background: var(--vscode-toolbar-hoverBackground, #2a2d2e);
}

.workflow-hierarchy-outline {
position: fixed;
top: 78px;
left: 8px;
bottom: 48px;
z-index: 30;
width: 300px;
display: flex;
flex-direction: column;
border: 1px solid var(--vscode-editorWidget-border, #454545);
border-radius: 4px;
background: var(--vscode-editorWidget-background, #252526);
color: var(--vscode-foreground, #cccccc);
font-family: var(--vscode-font-family);
font-size: 12px;
}

.workflow-hierarchy-outline[hidden],
.workflow-hierarchy-outline-toggle[hidden] {
display: none;
}

.workflow-hierarchy-outline-header {
display: flex;
align-items: center;
gap: 2px;
padding: 4px 6px;
border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
}

.workflow-hierarchy-outline-title {
flex: 1;
font-weight: 600;
text-transform: uppercase;
font-size: 11px;
opacity: 0.8;
}

.workflow-hierarchy-outline-btn {
border: none;
background: transparent;
color: inherit;
cursor: pointer;
padding: 2px;
}

.workflow-hierarchy-outline-search {
margin: 6px;
padding: 3px 6px;
border: 1px solid var(--vscode-input-border, #3c3c3c);
background: var(--vscode-input-background, #3c3c3c);
color: var(--vscode-input-foreground, #cccccc);
}

.workflow-hierarchy-outline-body {
flex: 1;
overflow: auto;
padding-bottom: 6px;
}

.workflow-hierarchy-outline-row {
display: flex;
align-items: center;
gap: 4px;
min-height: 22px;
padding-right: 6px;
cursor: pointer;
white-space: nowrap;
}

.workflow-hierarchy-outline-row:hover {
background: var(--vscode-list-hoverBackground, #2a2d2e);
}

.workflow-hierarchy-outline-row.current {
background: var(--vscode-list-activeSelectionBackground, #04395e);
color: var(--vscode-list-activeSelectionForeground, #ffffff);
}

.workflow-hierarchy-outline-twisty {
width: 16px;
flex: none;
}

.workflow-hierarchy-outline-label {
overflow: hidden;
text-overflow: ellipsis;
}

.workflow-hierarchy-outline-type,
.workflow-hierarchy-outline-meta {
opacity: 0.6;
}

.workflow-hierarchy-outline-file {
padding: 0 4px;
border-radius: 3px;
background: var(--vscode-badge-background, #4d4d4d);
color: var(--vscode-badge-foreground, #ffffff);
font-size: 10px;
}

.workflow-hierarchy-outline-meta {
margin-left: auto;
}

.workflow-hierarchy-outline-meta.error {
color: var(--vscode-errorForeground, #f48771);
opacity: 1;
}
82 changes: 82 additions & 0 deletions packages/diagram-client/src/hierarchy-outline-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Pure helpers for the hierarchy outline, kept GLSP-free so they are unit
* testable headlessly (the panel imports GLSP values and cannot load without a
* DOM) -- the split mlir-viewer's outline uses, for the same reason.
*/
import type { HierarchyOutlineEntry } from '@dialogram/shared';

export interface OutlineCrumb {
sourceUri: string;
workflowName: string;
workflowInstanceName?: string;
}

/** A row's stable id: its path of instance names (`''` for the root). */
export function rowId(entry: { path: string[] }): string {
return entry.path.join('/');
}

/** What a row is called: its instance name, or the workflow for the root. */
export function rowLabel(entry: HierarchyOutlineEntry): string {
return entry.path.length > 0 ? entry.path[entry.path.length - 1] : entry.workflowName;
}

/**
* A row survives a filter when it matches, or when something beneath it does:
* a filter that hid the ancestors would leave the matches unreachable, since
* the tree is the way to them. `query` is lower-cased and trimmed already.
*/
export function outlineMatches(entry: HierarchyOutlineEntry, query: string): boolean {
if (!query) {
return true;
}
if (rowLabel(entry).toLowerCase().includes(query) || entry.workflowName.toLowerCase().includes(query)) {
return true;
}
return entry.children.some(child => outlineMatches(child, query));
}

/** The entry at `path`, if the outline has it. */
export function entryAt(root: HierarchyOutlineEntry, path: string[]): HierarchyOutlineEntry | undefined {
let current: HierarchyOutlineEntry | undefined = root;
for (const name of path) {
current = current?.children.find(child => child.path[child.path.length - 1] === name);
}
return current;
}

/**
* The navigation trail to the instance at `path`: the root, then one crumb per
* instance on the way. What a breadcrumb and a drill-down produce, so a view
* opened from the outline is the same view either way would open.
*/
export function trailTo(root: HierarchyOutlineEntry, rootSourceUri: string, path: string[]): OutlineCrumb[] | undefined {
const trail: OutlineCrumb[] = [{ sourceUri: root.sourceUri ?? rootSourceUri, workflowName: root.workflowName }];
for (let i = 1; i <= path.length; i++) {
const entry = entryAt(root, path.slice(0, i));
if (!entry?.sourceUri) {
return undefined;
}
trail.push({ sourceUri: entry.sourceUri, workflowName: entry.workflowName, workflowInstanceName: path[i - 1] });
}
return trail;
}

/** The path the shown view is at, from its trail. */
export function pathOfTrail(trail: Array<{ workflowName: string; workflowInstanceName?: string }>): string[] {
return trail.slice(1).map(crumb => crumb.workflowInstanceName ?? crumb.workflowName);
}

/** The ids of every ancestor of `path`, so the current row can be revealed. */
export function ancestorIds(path: string[]): string[] {
return path.map((_, i) => path.slice(0, i).join('/'));
}

/** The file name of an instance defined elsewhere than the root, or nothing. */
export function foreignFileName(entry: HierarchyOutlineEntry, rootSourceUri: string | undefined): string | undefined {
if (!entry.sourceUri || !rootSourceUri || entry.sourceUri === rootSourceUri) {
return undefined;
}
const name = entry.sourceUri.split('/').pop();
return name ? decodeURIComponent(name) : undefined;
}
Loading
Loading