@danypops/pi-papyrus 0.43.1 → 0.43.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/extension/src/artifact-browser.ts +54 -31
  2. package/extension/src/artifact-detail-view.ts +52 -42
  3. package/extension/src/artifact-format.ts +11 -5
  4. package/extension/src/artifact-relationship-lines.ts +4 -5
  5. package/extension/src/beautiful-mermaid-renderer.ts +3 -5
  6. package/extension/src/context-budget.ts +8 -5
  7. package/extension/src/context-hub-contribution.ts +6 -2
  8. package/extension/src/context-injection-telemetry.ts +2 -2
  9. package/extension/src/discuss-ask-layout.ts +3 -1
  10. package/extension/src/discuss-ask-view.ts +437 -111
  11. package/extension/src/discuss.ts +64 -15
  12. package/extension/src/discussion-detail-view.ts +44 -22
  13. package/extension/src/docs.ts +3 -2
  14. package/extension/src/domain-tools.ts +79 -34
  15. package/extension/src/index.ts +170 -66
  16. package/extension/src/markdown.ts +3 -7
  17. package/extension/src/note-widget.ts +1 -1
  18. package/extension/src/notes.ts +3 -8
  19. package/extension/src/playbook-bridge.ts +17 -6
  20. package/extension/src/playbooks.ts +17 -7
  21. package/extension/src/rules.ts +5 -5
  22. package/extension/src/service-client.ts +23 -8
  23. package/extension/src/skill-catalog-footprint.ts +1 -1
  24. package/extension/src/task-detail-format.ts +9 -9
  25. package/extension/src/task-detail-view.ts +36 -29
  26. package/extension/src/task-focus-events.ts +3 -2
  27. package/extension/src/task-graph.ts +16 -12
  28. package/extension/src/task-presentation.ts +2 -6
  29. package/extension/src/task-widget.ts +12 -8
  30. package/extension/src/tasks.ts +148 -57
  31. package/extension/src/tool-rendering/artifact-card.ts +16 -4
  32. package/extension/src/tool-rendering/artifact-list.ts +23 -24
  33. package/extension/src/tool-rendering/index.ts +2 -6
  34. package/extension/src/tool-rendering/render-model.ts +96 -56
  35. package/extension/src/vehicle-artifact-renderers.ts +102 -0
  36. package/extension/src/vehicle-notes-client.ts +35 -7
  37. package/package.json +5 -5
@@ -1,5 +1,18 @@
1
- import { connectPushChannel, createRetryingClient, type PushChannelClient, type PushChannelState, type RetryingClient } from "@danypops/vehicle-client/daemon-client";
2
- import { connectPapyrusClient, resolvePushChannelTarget, resolveVehicleClientTarget, type OperationName, type PapyrusClient, type VehicleClientTarget } from "@danypops/papyrus";
1
+ import {
2
+ connectPapyrusClient,
3
+ type OperationName,
4
+ type PapyrusClient,
5
+ resolvePushChannelTarget,
6
+ resolveVehicleClientTarget,
7
+ type VehicleClientTarget,
8
+ } from "@danypops/papyrus";
9
+ import {
10
+ connectPushChannel,
11
+ createRetryingClient,
12
+ type PushChannelClient,
13
+ type PushChannelState,
14
+ type RetryingClient,
15
+ } from "@danypops/vehicle-client/daemon-client";
3
16
 
4
17
  type ClientConnector = () => Promise<PapyrusClient>;
5
18
 
@@ -10,10 +23,7 @@ export async function papyrusClient(): Promise<PapyrusClient> {
10
23
  return client.call(async (resolved) => resolved);
11
24
  }
12
25
 
13
- export async function callService<Input extends Record<string, unknown>, Output>(
14
- operation: OperationName,
15
- input: Input,
16
- ): Promise<Output> {
26
+ export async function callService<Input extends Record<string, unknown>, Output>(operation: OperationName, input: Input): Promise<Output> {
17
27
  return client.call((resolved) => resolved.call<Input, Output>(operation, input));
18
28
  }
19
29
 
@@ -66,7 +76,10 @@ export function currentVehicleClientTarget(): VehicleClientTarget | undefined {
66
76
  * tolerates "daemon not running" and falls back to its existing poll. A caller
67
77
  * should retry this on a later poll tick once the daemon is confirmed reachable.
68
78
  */
69
- export function subscribeTaskPushChannel(onMessage: () => void, onStateChange?: (state: PushChannelState) => void): PushChannelClient | undefined {
79
+ export function subscribeTaskPushChannel(
80
+ onMessage: () => void,
81
+ onStateChange?: (state: PushChannelState) => void,
82
+ ): PushChannelClient | undefined {
70
83
  const target = pushChannelTargetResolver();
71
84
  if (!target) return undefined;
72
85
  return connectPushChannel({
@@ -80,7 +93,9 @@ export function subscribeTaskPushChannel(onMessage: () => void, onStateChange?:
80
93
  },
81
94
  token: target.token,
82
95
  topics: ["tasks"],
83
- onMessage: (topic) => { if (topic === "tasks") onMessage(); },
96
+ onMessage: (topic) => {
97
+ if (topic === "tasks") onMessage();
98
+ },
84
99
  onStateChange,
85
100
  });
86
101
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN } from "@danypops/papyrus";
4
4
 
@@ -1,4 +1,4 @@
1
- import { checklistEntries, type Artifact, type ProofReference, type TaskEvent } from "@danypops/papyrus";
1
+ import { type Artifact, checklistEntries, type ProofReference, type TaskEvent } from "@danypops/papyrus";
2
2
  import { formatMetadata } from "./artifact-format.ts";
3
3
 
4
4
  const TASK_STATUS_GLYPHS: Record<string, string> = {
@@ -39,9 +39,9 @@ function gateLines(value: unknown): string[] {
39
39
  continue;
40
40
  }
41
41
  const record = gate as Record<string, unknown>;
42
- const type = typeof record["type"] === "string" ? record["type"] : "unknown";
43
- const target = typeof record["target"] === "string" ? record["target"] : "missing target";
44
- const expect = typeof record["expect"] === "string" ? ` · ${record["expect"]}` : "";
42
+ const type = typeof record.type === "string" ? record.type : "unknown";
43
+ const target = typeof record.target === "string" ? record.target : "missing target";
44
+ const expect = typeof record.expect === "string" ? ` · ${record.expect}` : "";
45
45
  lines.push(` ○ ${type} · ${target}${expect}`);
46
46
  }
47
47
  return lines;
@@ -59,9 +59,9 @@ function historyLines(history: TaskEvent[]): string[] {
59
59
  for (const value of event.evidence.gates) {
60
60
  if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
61
61
  const result = value as Record<string, unknown>;
62
- const gate = typeof result["gate"] === "object" && result["gate"] !== null ? result["gate"] as Record<string, unknown> : {};
63
- const passed = result["passed"] === true;
64
- lines.push(` ${passed ? "✓" : "✗"} ${String(gate["type"] ?? "gate")} · ${String(gate["target"] ?? "unknown")}`);
62
+ const gate = typeof result.gate === "object" && result.gate !== null ? (result.gate as Record<string, unknown>) : {};
63
+ const passed = result.passed === true;
64
+ lines.push(` ${passed ? "✓" : "✗"} ${String(gate.type ?? "gate")} · ${String(gate.target ?? "unknown")}`);
65
65
  }
66
66
  }
67
67
  }
@@ -78,9 +78,9 @@ export interface TaskDetailContent {
78
78
 
79
79
  export function taskDetailContent(task: Artifact, history: TaskEvent[] = []): TaskDetailContent {
80
80
  const sections: string[][] = [];
81
- const checklist = checklistLines(task.extra["checklist"]);
81
+ const checklist = checklistLines(task.extra.checklist);
82
82
  if (checklist.length > 0) sections.push(checklist);
83
- const gates = gateLines(task.extra["gates"]);
83
+ const gates = gateLines(task.extra.gates);
84
84
  if (gates.length > 0) sections.push(gates);
85
85
  const metadata = Object.fromEntries(Object.entries(task.extra).filter(([key]) => key !== "checklist" && key !== "gates"));
86
86
  if (Object.keys(metadata).length > 0) sections.push(["Metadata:", ...formatMetadata(metadata).map((line) => ` ${line}`)]);
@@ -1,20 +1,20 @@
1
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, type TUI } from "@earendil-works/pi-tui";
3
- import { buildDetailLines, type DetailField, type DetailSection } from "malevich-tui-components";
4
1
  import {
2
+ type Artifact,
3
+ type GraphRenderer,
4
+ projectTaskRelationships,
5
5
  TASK_DETAIL_HORIZONTAL_PAN_COLUMNS,
6
6
  TASK_DETAIL_MAX_VISIBLE_LINES,
7
7
  TASK_DETAIL_MIN_VISIBLE_LINES,
8
8
  TASK_DETAIL_RESERVED_ROWS,
9
- projectTaskRelationships,
10
- type Artifact,
11
- type GraphRenderer,
12
9
  type TaskEvent,
13
10
  type TaskGraph,
14
11
  } from "@danypops/papyrus";
12
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
13
+ import { matchesKey, sliceByColumn, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
14
+ import { buildDetailLines, type DetailField, type DetailSection } from "malevich-tui-components";
15
15
  import { BeautifulMermaidRenderer } from "./beautiful-mermaid-renderer.ts";
16
- import { taskDetailContent, taskDetailsText, type TaskDetailContent } from "./task-detail-format.ts";
17
- import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
16
+ import { type ActiveTheme, renderMarkdownBody } from "./markdown.ts";
17
+ import { type TaskDetailContent, taskDetailContent, taskDetailsText } from "./task-detail-format.ts";
18
18
  import { TASK_STATUS_PRESENTATION } from "./task-presentation.ts";
19
19
 
20
20
  interface DetailLine {
@@ -47,7 +47,9 @@ class TaskDetailViewport {
47
47
  this.status = task.status;
48
48
  }
49
49
 
50
- invalidate(): void { this.renderedWidth = 0; }
50
+ invalidate(): void {
51
+ this.renderedWidth = 0;
52
+ }
51
53
 
52
54
  render(width: number): string[] {
53
55
  const contentWidth = Math.max(1, width - 2);
@@ -62,21 +64,28 @@ class TaskDetailViewport {
62
64
  graphWidth > contentWidth ? `←/→ graph · column ${this.offsetX + 1}/${graphWidth}` : "",
63
65
  this.detailLines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.detailLines.length}` : "",
64
66
  "Esc back",
65
- ].filter(Boolean).join(" · ");
67
+ ]
68
+ .filter(Boolean)
69
+ .join(" · ");
66
70
  return [
67
71
  border,
68
72
  truncateToWidth(theme.fg("accent", theme.bold("Task details")), width, ""),
69
73
  border,
70
- ...this.detailLines.slice(this.offsetY, end).map((line) => line.graph
71
- ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
72
- : truncateToWidth(` ${line.text}`, width, "")),
74
+ ...this.detailLines
75
+ .slice(this.offsetY, end)
76
+ .map((line) =>
77
+ line.graph ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}` : truncateToWidth(` ${line.text}`, width, ""),
78
+ ),
73
79
  truncateToWidth(theme.fg("dim", footer), width, ""),
74
80
  border,
75
81
  ];
76
82
  }
77
83
 
78
84
  handleInput(data: string): void {
79
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
85
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
86
+ this.close();
87
+ return;
88
+ }
80
89
  if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
81
90
  else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.detailLines.length - this.visibleLines), this.offsetY + 1);
82
91
  else if (matchesKey(data, "left")) this.offsetX = Math.max(0, this.offsetX - TASK_DETAIL_HORIZONTAL_PAN_COLUMNS);
@@ -111,21 +120,18 @@ class TaskDetailViewport {
111
120
  line: (s: string) => theme.fg("dim", s),
112
121
  };
113
122
  const fields: DetailField[] = this.content.labels.length > 0 ? [{ label: "Labels", value: this.content.labels.join(", ") }] : [];
114
- const labels = fields.length > 0
115
- ? [...buildDetailLines(width, { fields, theme: detailTheme }).map((text) => ({ text, graph: false })), { text: "", graph: false }]
116
- : [{ text: "", graph: false }];
123
+ const labels =
124
+ fields.length > 0
125
+ ? [...buildDetailLines(width, { fields, theme: detailTheme }).map((text) => ({ text, graph: false })), { text: "", graph: false }]
126
+ : [{ text: "", graph: false }];
117
127
  const body = renderMarkdownBody(this.content.body, width, this.activeTheme).map((text) => ({ text, graph: false }));
118
128
  const sections: DetailSection[] = this.content.sections.map((section) => ({ heading: section[0], lines: section.slice(1) }));
119
- const sectionLines = sections.length > 0
120
- ? buildDetailLines(width, { sections, theme: detailTheme }).map((text) => ({ text, graph: false }))
121
- : [];
122
- const relationshipHeader = this.graphLines.length > 0
123
- ? [
124
- { text: "", graph: false },
125
- ...wrap("Relationships:", "muted"),
126
- ...wrap(" Dependencies point prerequisite → dependent.", "dim"),
127
- ]
128
- : [];
129
+ const sectionLines =
130
+ sections.length > 0 ? buildDetailLines(width, { sections, theme: detailTheme }).map((text) => ({ text, graph: false })) : [];
131
+ const relationshipHeader =
132
+ this.graphLines.length > 0
133
+ ? [{ text: "", graph: false }, ...wrap("Relationships:", "muted"), ...wrap(" Dependencies point prerequisite → dependent.", "dim")]
134
+ : [];
129
135
  this.detailLines = [
130
136
  ...identity,
131
137
  ...labels,
@@ -151,6 +157,7 @@ export async function showTaskDetails(
151
157
  ctx.ui.notify(content, "info");
152
158
  return;
153
159
  }
154
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
155
- new TaskDetailViewport(tui, () => ctx.ui.theme ?? theme, task, relationshipGraph, history, done));
160
+ await ctx.ui.custom<void>(
161
+ (tui, theme, _keybindings, done) => new TaskDetailViewport(tui, () => ctx.ui.theme ?? theme, task, relationshipGraph, history, done),
162
+ );
156
163
  }
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
1
  import { PAPYRUS_TASK_FOCUS_CHANNEL, PAPYRUS_TASK_FOCUS_SCHEMA } from "@danypops/papyrus";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  export type TaskFocusStatus = "focused" | "paused" | "unpaused" | "cleared";
5
5
 
@@ -27,7 +27,8 @@ export interface TaskFocusEventInput {
27
27
  * Papyrus depending on them.
28
28
  */
29
29
  export function buildTaskFocusEvent(input: TaskFocusEventInput): TaskFocusEvent {
30
- if (input.status !== "cleared" && input.taskId === null) throw new Error(`task-focus event of status "${input.status}" requires a taskId`);
30
+ if (input.status !== "cleared" && input.taskId === null)
31
+ throw new Error(`task-focus event of status "${input.status}" requires a taskId`);
31
32
  return {
32
33
  schema: PAPYRUS_TASK_FOCUS_SCHEMA,
33
34
  taskId: input.taskId,
@@ -1,15 +1,15 @@
1
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
2
- import { matchesKey, sliceByColumn, truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
3
1
  import {
2
+ type GraphRenderer,
3
+ projectTaskGraph,
4
4
  TASK_GRAPH_HORIZONTAL_PAN_COLUMNS,
5
5
  TASK_GRAPH_MAX_VISIBLE_LINES,
6
6
  TASK_GRAPH_MIN_VISIBLE_LINES,
7
7
  TASK_GRAPH_RESERVED_ROWS,
8
- projectTaskGraph,
9
- type GraphRenderer,
10
8
  type TaskGraph,
11
9
  type TaskGraphView,
12
10
  } from "@danypops/papyrus";
11
+ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
12
+ import { matchesKey, sliceByColumn, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
13
13
  import { BeautifulMermaidRenderer } from "./beautiful-mermaid-renderer.ts";
14
14
  import { TASK_STATUS_PRESENTATION } from "./task-presentation.ts";
15
15
 
@@ -53,22 +53,27 @@ export class TaskGraphViewport {
53
53
  this.offsetY = Math.min(this.offsetY, Math.max(0, this.graphLines.length - this.viewportHeight));
54
54
  const end = Math.min(this.graphLines.length, this.offsetY + this.viewportHeight);
55
55
  const border = this.theme.fg("borderMuted", "─".repeat(contentWidth));
56
- const position = graphWidth > contentWidth || this.graphLines.length > this.viewportHeight
57
- ? ` · column ${this.offsetX + 1}/${Math.max(contentWidth, graphWidth)} · row ${this.offsetY + 1}/${this.graphLines.length}`
58
- : "";
56
+ const position =
57
+ graphWidth > contentWidth || this.graphLines.length > this.viewportHeight
58
+ ? ` · column ${this.offsetX + 1}/${Math.max(contentWidth, graphWidth)} · row ${this.offsetY + 1}/${this.graphLines.length}`
59
+ : "";
59
60
  return [
60
61
  border,
61
62
  truncateToWidth(this.theme.bold(`Task graph · ${GRAPH_VIEWS[this.viewIndex]}`), contentWidth, ""),
62
63
  truncateToWidth(this.theme.fg("dim", `Tab switch · arrows pan · Esc back${position}`), contentWidth, ""),
63
64
  border,
64
- ...this.graphLines.slice(this.offsetY, end).map((line) =>
65
- colorizeTaskGraphLine(this.theme, sliceByColumn(line, this.offsetX, contentWidth, true))),
65
+ ...this.graphLines
66
+ .slice(this.offsetY, end)
67
+ .map((line) => colorizeTaskGraphLine(this.theme, sliceByColumn(line, this.offsetX, contentWidth, true))),
66
68
  border,
67
69
  ];
68
70
  }
69
71
 
70
72
  handleInput(data: string): void {
71
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
73
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
74
+ this.close();
75
+ return;
76
+ }
72
77
  if (matchesKey(data, "tab")) this.switchView();
73
78
  else if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
74
79
  else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.graphLines.length - this.viewportHeight), this.offsetY + 1);
@@ -112,6 +117,5 @@ export async function showTaskGraph(
112
117
  ctx.ui.notify(rendered.lines.join("\n") || "No tasks in the execution graph", "info");
113
118
  return;
114
119
  }
115
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
116
- new TaskGraphViewport(tui, theme, graph, renderer, done));
120
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new TaskGraphViewport(tui, theme, graph, renderer, done));
117
121
  }
@@ -1,5 +1,5 @@
1
- import type { ThemeColor } from "@earendil-works/pi-coding-agent";
2
1
  import type { TaskStatus } from "@danypops/papyrus";
2
+ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  export interface TaskStatusPresentation {
5
5
  label: string;
@@ -16,11 +16,7 @@ export const TASK_STATUS_PRESENTATION: Record<TaskStatus, TaskStatusPresentation
16
16
  canceled: { label: "canceled", glyph: "×", color: "error" },
17
17
  };
18
18
 
19
- export function taskTreeConnector(options: {
20
- depth: number;
21
- hasChildren: boolean;
22
- hasLaterSibling: boolean;
23
- }): string {
19
+ export function taskTreeConnector(options: { depth: number; hasChildren: boolean; hasLaterSibling: boolean }): string {
24
20
  if (options.depth === 0) return options.hasChildren ? "▾" : "·";
25
21
  return `${"│ ".repeat(Math.max(0, options.depth - 1))}${options.hasLaterSibling ? "├─" : "└─"}`;
26
22
  }
@@ -1,4 +1,4 @@
1
- import { TASK_WIDGET_OPEN_LIMIT, type Artifact, type TaskGraph } from "@danypops/papyrus";
1
+ import { type Artifact, TASK_WIDGET_OPEN_LIMIT, type TaskGraph } from "@danypops/papyrus";
2
2
 
3
3
  export interface TaskWidgetRow {
4
4
  task: Artifact;
@@ -32,10 +32,7 @@ function isOpen(task: Artifact): boolean {
32
32
  }
33
33
 
34
34
  /** Keep bounded actionable work in containment order while always retaining active focus. */
35
- export function buildTaskWidgetProjection(
36
- graph: TaskGraph,
37
- openLimit = TASK_WIDGET_OPEN_LIMIT,
38
- ): TaskWidgetProjection {
35
+ export function buildTaskWidgetProjection(graph: TaskGraph, openLimit = TASK_WIDGET_OPEN_LIMIT): TaskWidgetProjection {
39
36
  const byId = new Map(graph.nodes.map((node) => [node.task.id, node]));
40
37
  const visited = new Set<string>();
41
38
  const ordered: TaskWidgetRow[] = [];
@@ -46,7 +43,15 @@ export function buildTaskWidgetProjection(
46
43
  if (!node) return;
47
44
  visited.add(id);
48
45
  const open = isOpen(node.task);
49
- if (open) ordered.push({ task: node.task, depth: openDepth, hasOpenChildren: false, active: node.active === true, focusStatus: node.focusStatus, parentCount: node.parentIds.length });
46
+ if (open)
47
+ ordered.push({
48
+ task: node.task,
49
+ depth: openDepth,
50
+ hasOpenChildren: false,
51
+ active: node.active === true,
52
+ focusStatus: node.focusStatus,
53
+ parentCount: node.parentIds.length,
54
+ });
50
55
  const childDepth = open ? openDepth + 1 : openDepth;
51
56
  for (const childId of node.childIds) visit(childId, childDepth);
52
57
  };
@@ -61,8 +66,7 @@ export function buildTaskWidgetProjection(
61
66
  let rows = ordered.slice(0, limit);
62
67
  const active = ordered.find((row) => row.active);
63
68
  if (active && !rows.some((row) => row.task.id === active.task.id) && limit > 0) {
64
- rows = [...rows.slice(0, Math.max(0, limit - 1)), active]
65
- .sort((left, right) => ordered.indexOf(left) - ordered.indexOf(right));
69
+ rows = [...rows.slice(0, Math.max(0, limit - 1)), active].sort((left, right) => ordered.indexOf(left) - ordered.indexOf(right));
66
70
  }
67
71
  return { rows, openTotal: ordered.length, total: graph.nodes.length, scopeLabel: graph.scope?.label ?? "All projects" };
68
72
  }