@danypops/pi-papyrus 0.43.1 → 0.43.3
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.
- package/extension/src/artifact-browser.ts +54 -31
- package/extension/src/artifact-detail-view.ts +52 -42
- package/extension/src/artifact-format.ts +11 -5
- package/extension/src/artifact-relationship-lines.ts +4 -5
- package/extension/src/beautiful-mermaid-renderer.ts +3 -5
- package/extension/src/context-budget.ts +8 -5
- package/extension/src/context-hub-contribution.ts +6 -2
- package/extension/src/context-injection-telemetry.ts +2 -2
- package/extension/src/discuss-ask-layout.ts +3 -1
- package/extension/src/discuss-ask-view.ts +437 -111
- package/extension/src/discuss.ts +64 -15
- package/extension/src/discussion-detail-view.ts +44 -22
- package/extension/src/docs.ts +3 -2
- package/extension/src/domain-tools.ts +79 -34
- package/extension/src/index.ts +170 -66
- package/extension/src/markdown.ts +3 -7
- package/extension/src/note-widget.ts +1 -1
- package/extension/src/notes.ts +3 -8
- package/extension/src/playbook-bridge.ts +17 -6
- package/extension/src/playbooks.ts +17 -7
- package/extension/src/rules.ts +5 -5
- package/extension/src/service-client.ts +23 -8
- package/extension/src/skill-catalog-footprint.ts +1 -1
- package/extension/src/task-detail-format.ts +9 -9
- package/extension/src/task-detail-view.ts +36 -29
- package/extension/src/task-focus-events.ts +3 -2
- package/extension/src/task-graph.ts +16 -12
- package/extension/src/task-presentation.ts +2 -6
- package/extension/src/task-widget.ts +12 -8
- package/extension/src/tasks.ts +148 -57
- package/extension/src/tool-rendering/artifact-card.ts +1 -4
- package/extension/src/tool-rendering/artifact-list.ts +23 -24
- package/extension/src/tool-rendering/index.ts +2 -6
- package/extension/src/tool-rendering/render-model.ts +69 -55
- package/extension/src/vehicle-artifact-renderers.ts +58 -0
- package/extension/src/vehicle-notes-client.ts +35 -7
- package/package.json +5 -5
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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(
|
|
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) => {
|
|
96
|
+
onMessage: (topic) => {
|
|
97
|
+
if (topic === "tasks") onMessage();
|
|
98
|
+
},
|
|
84
99
|
onStateChange,
|
|
85
100
|
});
|
|
86
101
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
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 {
|
|
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
|
|
43
|
-
const target = typeof record
|
|
44
|
-
const expect = typeof record
|
|
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
|
|
63
|
-
const passed = result
|
|
64
|
-
lines.push(` ${passed ? "✓" : "✗"} ${String(gate
|
|
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
|
|
81
|
+
const checklist = checklistLines(task.extra.checklist);
|
|
82
82
|
if (checklist.length > 0) sections.push(checklist);
|
|
83
|
-
const gates = gateLines(task.extra
|
|
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 {
|
|
17
|
-
import {
|
|
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 {
|
|
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
|
-
]
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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")) {
|
|
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 =
|
|
115
|
-
|
|
116
|
-
|
|
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 =
|
|
120
|
-
? buildDetailLines(width, { sections, theme: detailTheme }).map((text) => ({ text, graph: false }))
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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>(
|
|
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)
|
|
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 =
|
|
57
|
-
|
|
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
|
|
65
|
-
|
|
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")) {
|
|
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 {
|
|
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)
|
|
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
|
}
|