@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,10 +1,9 @@
|
|
|
1
|
+
import { type Artifact, type OperationName, SEED_RELATIONS } from "@danypops/papyrus";
|
|
1
2
|
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { SEED_RELATIONS, type Artifact, type OperationName } from "@danypops/papyrus";
|
|
5
|
-
import type { StatusPresentation } from "./artifact-status-presentation.ts";
|
|
6
|
-
import { artifactDetailsText } from "./artifact-detail-format.ts";
|
|
7
5
|
import { showArtifactDetailView } from "./artifact-detail-view.ts";
|
|
6
|
+
import type { StatusPresentation } from "./artifact-status-presentation.ts";
|
|
8
7
|
import { callService } from "./service-client.ts";
|
|
9
8
|
|
|
10
9
|
export { artifactDetailsText } from "./artifact-detail-format.ts";
|
|
@@ -29,14 +28,11 @@ export interface ArtifactBrowserConfig {
|
|
|
29
28
|
export function filterArtifactRows(rows: Artifact[], query: string): Artifact[] {
|
|
30
29
|
const needle = query.trim().toLowerCase();
|
|
31
30
|
if (!needle) return [...rows];
|
|
32
|
-
return rows.filter((row) =>
|
|
33
|
-
row.id,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
row.labels.join(" "),
|
|
38
|
-
JSON.stringify(row.extra),
|
|
39
|
-
].some((value) => value.toLowerCase().includes(needle)));
|
|
31
|
+
return rows.filter((row) =>
|
|
32
|
+
[row.id, row.title, row.body, row.subtype, row.labels.join(" "), JSON.stringify(row.extra)].some((value) =>
|
|
33
|
+
value.toLowerCase().includes(needle),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
export function statusSummary(rows: Artifact[], order: string[]): Array<{ status: string; count: number }> {
|
|
@@ -53,10 +49,7 @@ async function loadArtifacts(config: ArtifactBrowserConfig): Promise<Artifact[]>
|
|
|
53
49
|
});
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
export type ArtifactDetailLoader = (
|
|
57
|
-
operation: OperationName,
|
|
58
|
-
input: Record<string, unknown>,
|
|
59
|
-
) => Promise<Artifact | null>;
|
|
52
|
+
export type ArtifactDetailLoader = (operation: OperationName, input: Record<string, unknown>) => Promise<Artifact | null>;
|
|
60
53
|
|
|
61
54
|
const loadArtifactDetails: ArtifactDetailLoader = (operation, input) =>
|
|
62
55
|
callService<Record<string, unknown>, Artifact | null>(operation, input);
|
|
@@ -76,7 +69,10 @@ export async function showArtifactDetails(
|
|
|
76
69
|
depth: DETAIL_GRAPH_DEPTH,
|
|
77
70
|
max_nodes: DETAIL_GRAPH_NODES,
|
|
78
71
|
});
|
|
79
|
-
if (!artifact) {
|
|
72
|
+
if (!artifact) {
|
|
73
|
+
ctx.ui.notify("Artifact not found", "error");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
80
76
|
await showArtifactDetailView(ctx, artifact);
|
|
81
77
|
} catch (error) {
|
|
82
78
|
ctx.ui.notify(`Show details failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -86,7 +82,7 @@ export async function showArtifactDetails(
|
|
|
86
82
|
export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: string, fixedRelation?: string): Promise<void> {
|
|
87
83
|
const target = await ctx.ui.input("Target artifact id:", "");
|
|
88
84
|
if (!target) return;
|
|
89
|
-
const relation = fixedRelation ?? await ctx.ui.select("Relation", [...SEED_RELATIONS]);
|
|
85
|
+
const relation = fixedRelation ?? (await ctx.ui.select("Relation", [...SEED_RELATIONS]));
|
|
90
86
|
if (!relation) return;
|
|
91
87
|
try {
|
|
92
88
|
await callService("graph.link", { from: fromId, relation, to: target });
|
|
@@ -99,7 +95,10 @@ export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: str
|
|
|
99
95
|
export async function setArtifactStatus(ctx: ExtensionCommandContext, id: string, status: string): Promise<void> {
|
|
100
96
|
try {
|
|
101
97
|
const artifact = await callService<Record<string, unknown>, Artifact | null>("graph.status", { id, status });
|
|
102
|
-
if (!artifact) {
|
|
98
|
+
if (!artifact) {
|
|
99
|
+
ctx.ui.notify("Artifact not found", "error");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
103
102
|
ctx.ui.notify(`${artifact.title} → [${artifact.status}]`, "info");
|
|
104
103
|
} catch (error) {
|
|
105
104
|
ctx.ui.notify(`Status change failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -120,7 +119,10 @@ export async function showArtifactBrowser(ctx: ExtensionCommandContext, config:
|
|
|
120
119
|
for (;;) {
|
|
121
120
|
const selected = await renderPanel(ctx, rows, config);
|
|
122
121
|
if (selected === undefined) return;
|
|
123
|
-
if (selected === "refresh") {
|
|
122
|
+
if (selected === "refresh") {
|
|
123
|
+
rows = await loadArtifacts(config);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
124
126
|
const choices = config.actions(selected);
|
|
125
127
|
const choice = await ctx.ui.select(selected.title, choices);
|
|
126
128
|
if (!choice) continue;
|
|
@@ -151,8 +153,9 @@ function renderPanel(
|
|
|
151
153
|
const title = theme.bold(config.title);
|
|
152
154
|
const hint = searchActive
|
|
153
155
|
? rawKeyHint("esc", "clear")
|
|
154
|
-
: [rawKeyHint("enter", "actions"), rawKeyHint("/", "filter"), rawKeyHint("r", "refresh"), rawKeyHint("esc", "close")]
|
|
155
|
-
|
|
156
|
+
: [rawKeyHint("enter", "actions"), rawKeyHint("/", "filter"), rawKeyHint("r", "refresh"), rawKeyHint("esc", "close")].join(
|
|
157
|
+
theme.fg("muted", " · "),
|
|
158
|
+
);
|
|
156
159
|
const spacing = Math.max(1, width - visibleWidth(title) - visibleWidth(hint));
|
|
157
160
|
const summary = statusSummary(rows, config.statusOrder)
|
|
158
161
|
.map(({ status, count }) => {
|
|
@@ -205,20 +208,40 @@ function renderPanel(
|
|
|
205
208
|
invalidate: () => container.invalidate(),
|
|
206
209
|
handleInput(data: string) {
|
|
207
210
|
if (searchActive) {
|
|
208
|
-
if (data === "\x1b") {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
if (data === "\x1b") {
|
|
212
|
+
searchActive = false;
|
|
213
|
+
applyFilter();
|
|
214
|
+
} else if (data === "\r") searchActive = false;
|
|
215
|
+
else {
|
|
216
|
+
input.handleInput(data);
|
|
217
|
+
applyFilter();
|
|
218
|
+
}
|
|
211
219
|
tui.requestRender();
|
|
212
220
|
return;
|
|
213
221
|
}
|
|
214
222
|
switch (data) {
|
|
215
|
-
case "\x1b[A":
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
case "
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
case "\x1b[A":
|
|
224
|
+
selectedIndex = (selectedIndex - 1 + filtered.length) % Math.max(filtered.length, 1);
|
|
225
|
+
break;
|
|
226
|
+
case "\x1b[B":
|
|
227
|
+
selectedIndex = (selectedIndex + 1) % Math.max(filtered.length, 1);
|
|
228
|
+
break;
|
|
229
|
+
case "/":
|
|
230
|
+
searchActive = true;
|
|
231
|
+
break;
|
|
232
|
+
case "r":
|
|
233
|
+
done("refresh");
|
|
234
|
+
return;
|
|
235
|
+
case "\r": {
|
|
236
|
+
const row = filtered[selectedIndex];
|
|
237
|
+
if (row) done(row);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "\x1b":
|
|
241
|
+
done(undefined);
|
|
242
|
+
return;
|
|
243
|
+
default:
|
|
244
|
+
return;
|
|
222
245
|
}
|
|
223
246
|
tui.requestRender();
|
|
224
247
|
},
|
|
@@ -1,6 +1,3 @@
|
|
|
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 {
|
|
5
2
|
ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS,
|
|
6
3
|
ARTIFACT_DETAIL_MAX_VISIBLE_LINES,
|
|
@@ -9,10 +6,13 @@ import {
|
|
|
9
6
|
type Artifact,
|
|
10
7
|
type GraphRenderer,
|
|
11
8
|
} from "@danypops/papyrus";
|
|
12
|
-
import {
|
|
9
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { matchesKey, sliceByColumn, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
11
|
+
import { buildDetailLines, type DetailField, type DetailSection } from "malevich-tui-components";
|
|
12
|
+
import { type ArtifactDetailContent, artifactDetailContent, artifactDetailsText } from "./artifact-detail-format.ts";
|
|
13
13
|
import { buildArtifactRelationshipLines } from "./artifact-relationship-lines.ts";
|
|
14
14
|
import { BeautifulMermaidRenderer } from "./beautiful-mermaid-renderer.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { type ActiveTheme, renderMarkdownBody } from "./markdown.ts";
|
|
16
16
|
|
|
17
17
|
interface ArtifactDetailLine {
|
|
18
18
|
text: string;
|
|
@@ -41,7 +41,9 @@ class ArtifactDetailViewport {
|
|
|
41
41
|
this.content = artifactDetailContent(artifact, relationshipLines);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
invalidate(): void {
|
|
44
|
+
invalidate(): void {
|
|
45
|
+
this.renderedWidth = 0;
|
|
46
|
+
}
|
|
45
47
|
|
|
46
48
|
render(width: number): string[] {
|
|
47
49
|
const contentWidth = Math.max(1, width - 2);
|
|
@@ -56,21 +58,28 @@ class ArtifactDetailViewport {
|
|
|
56
58
|
wideWidth > contentWidth ? `←/→ relationships · column ${this.offsetX + 1}/${wideWidth}` : "",
|
|
57
59
|
this.lines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.lines.length}` : "",
|
|
58
60
|
"Esc back",
|
|
59
|
-
]
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(" · ");
|
|
60
64
|
return [
|
|
61
65
|
border,
|
|
62
66
|
truncateToWidth(theme.fg("accent", theme.bold("Artifact details")), width, ""),
|
|
63
67
|
border,
|
|
64
|
-
...this.lines
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
...this.lines
|
|
69
|
+
.slice(this.offsetY, end)
|
|
70
|
+
.map((line) =>
|
|
71
|
+
line.wide ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}` : truncateToWidth(` ${line.text}`, width, ""),
|
|
72
|
+
),
|
|
67
73
|
truncateToWidth(theme.fg("dim", footer), width, ""),
|
|
68
74
|
border,
|
|
69
75
|
];
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
handleInput(data: string): void {
|
|
73
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
79
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
80
|
+
this.close();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
74
83
|
if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
|
|
75
84
|
else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.lines.length - this.visibleLines), this.offsetY + 1);
|
|
76
85
|
else if (matchesKey(data, "left")) this.offsetX = Math.max(0, this.offsetX - ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS);
|
|
@@ -85,11 +94,7 @@ class ArtifactDetailViewport {
|
|
|
85
94
|
const theme = this.activeTheme();
|
|
86
95
|
const wrap = (text: string, color: "text" | "muted" | "dim" = "text"): ArtifactDetailLine[] =>
|
|
87
96
|
(text.length === 0 ? [""] : wrapTextWithAnsi(theme.fg(color, text), width)).map((line) => ({ text: line, wide: false }));
|
|
88
|
-
const identity = [
|
|
89
|
-
...wrap(theme.bold(this.content.title)),
|
|
90
|
-
...wrap(this.content.identity, "muted"),
|
|
91
|
-
{ text: "", wide: false },
|
|
92
|
-
];
|
|
97
|
+
const identity = [...wrap(theme.bold(this.content.title)), ...wrap(this.content.identity, "muted"), { text: "", wide: false }];
|
|
93
98
|
const body = renderMarkdownBody(this.content.body, width, this.activeTheme).map((text) => ({ text, wide: false }));
|
|
94
99
|
|
|
95
100
|
// Labels + Metadata are plain field/flat-line shapes -- delegated to malevich's
|
|
@@ -98,34 +103,35 @@ class ArtifactDetailViewport {
|
|
|
98
103
|
// either, and forcing them through it would either drop markdown formatting or
|
|
99
104
|
// lose the pan feature.
|
|
100
105
|
const fields: DetailField[] = this.content.labels.length > 0 ? [{ label: "Labels", value: this.content.labels.join(", ") }] : [];
|
|
101
|
-
const sections: DetailSection[] =
|
|
102
|
-
? [{ heading: "Metadata:", lines: this.content.metadata.map((line) => ` ${line}`) }]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
const sections: DetailSection[] =
|
|
107
|
+
this.content.metadata.length > 0 ? [{ heading: "Metadata:", lines: this.content.metadata.map((line) => ` ${line}`) }] : [];
|
|
108
|
+
const labelsAndMetadata =
|
|
109
|
+
fields.length > 0 || sections.length > 0
|
|
110
|
+
? buildDetailLines(width, {
|
|
111
|
+
fields,
|
|
112
|
+
sections,
|
|
113
|
+
theme: {
|
|
114
|
+
field: (s) => theme.fg("muted", s),
|
|
115
|
+
heading: (s) => theme.fg("muted", s),
|
|
116
|
+
byline: (s) => theme.fg("dim", s),
|
|
117
|
+
body: (s) => theme.fg("text", s),
|
|
118
|
+
line: (s) => theme.fg("dim", s),
|
|
119
|
+
},
|
|
120
|
+
}).map((text) => ({ text, wide: false }))
|
|
121
|
+
: [];
|
|
117
122
|
// buildDetailLines' fields/sections don't insert a leading blank before the
|
|
118
123
|
// first field the way the original hand-rolled labels block did -- add it back
|
|
119
124
|
// when either piece rendered anything, matching the original layout exactly.
|
|
120
125
|
const labelsAndMetadataWithLeadingBlank = fields.length > 0 ? [{ text: "", wide: false }, ...labelsAndMetadata] : labelsAndMetadata;
|
|
121
126
|
|
|
122
|
-
const relationships =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
const relationships =
|
|
128
|
+
this.content.relationships.length > 0
|
|
129
|
+
? [
|
|
130
|
+
{ text: "", wide: false },
|
|
131
|
+
...wrap("Relationships:", "muted"),
|
|
132
|
+
...this.content.relationships.map((text) => ({ text: theme.fg("text", text), wide: true })),
|
|
133
|
+
]
|
|
134
|
+
: [];
|
|
129
135
|
this.lines = [...identity, ...body, ...labelsAndMetadataWithLeadingBlank, ...relationships];
|
|
130
136
|
this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
|
|
131
137
|
}
|
|
@@ -138,7 +144,11 @@ export async function showArtifactDetailView(
|
|
|
138
144
|
): Promise<void> {
|
|
139
145
|
const relationshipLines = buildArtifactRelationshipLines(artifact, renderer);
|
|
140
146
|
const output = artifactDetailsText(artifact, relationshipLines);
|
|
141
|
-
if (ctx.mode !== "tui") {
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
if (ctx.mode !== "tui") {
|
|
148
|
+
ctx.ui.notify(output, "info");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
await ctx.ui.custom<void>(
|
|
152
|
+
(tui, theme, _keybindings, done) => new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, relationshipLines, done),
|
|
153
|
+
);
|
|
144
154
|
}
|
|
@@ -39,12 +39,15 @@ export function formatMetadata(value: unknown, options: MetadataFormatOptions =
|
|
|
39
39
|
if (Array.isArray(current)) {
|
|
40
40
|
const lines: string[] = [];
|
|
41
41
|
for (const item of current) {
|
|
42
|
-
if (renderedItems >= maxItems) {
|
|
42
|
+
if (renderedItems >= maxItems) {
|
|
43
|
+
lines.push(`${pad}…`);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
43
46
|
renderedItems++;
|
|
44
|
-
if (isRecord(item) && typeof item
|
|
45
|
-
const status = typeof item
|
|
47
|
+
if (isRecord(item) && typeof item.title === "string") {
|
|
48
|
+
const status = typeof item.status === "string" ? item.status : "";
|
|
46
49
|
const glyph = STATUS_GLYPHS[status];
|
|
47
|
-
lines.push(`${pad}- ${glyph ? `${glyph} ` : ""}${item
|
|
50
|
+
lines.push(`${pad}- ${glyph ? `${glyph} ` : ""}${item.title}`);
|
|
48
51
|
const rest = Object.fromEntries(Object.entries(item).filter(([key]) => key !== "title" && key !== "status"));
|
|
49
52
|
if (Object.keys(rest).length > 0) lines.push(...render(rest, indent + 1, depth + 1));
|
|
50
53
|
} else if (Array.isArray(item) || isRecord(item)) {
|
|
@@ -60,7 +63,10 @@ export function formatMetadata(value: unknown, options: MetadataFormatOptions =
|
|
|
60
63
|
if (isRecord(current)) {
|
|
61
64
|
const lines: string[] = [];
|
|
62
65
|
for (const [key, item] of Object.entries(current)) {
|
|
63
|
-
if (renderedItems >= maxItems) {
|
|
66
|
+
if (renderedItems >= maxItems) {
|
|
67
|
+
lines.push(`${pad}…`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
64
70
|
renderedItems++;
|
|
65
71
|
if (Array.isArray(item) || isRecord(item)) {
|
|
66
72
|
lines.push(`${pad}${key}:`);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type Artifact,
|
|
2
3
|
GRAPH_RENDER_MAX_ROUTED_EDGES,
|
|
3
4
|
GRAPH_RENDER_MAX_ROUTED_NODES,
|
|
4
|
-
projectArtifactRelationships,
|
|
5
|
-
type Artifact,
|
|
6
5
|
type GraphRenderer,
|
|
6
|
+
projectArtifactRelationships,
|
|
7
7
|
} from "@danypops/papyrus";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -15,9 +15,8 @@ import {
|
|
|
15
15
|
export function buildArtifactRelationshipLines(artifact: Artifact, renderer: GraphRenderer): string[] {
|
|
16
16
|
const graph = projectArtifactRelationships(artifact);
|
|
17
17
|
if (graph.edges.length === 0) return [];
|
|
18
|
-
const withinBounds =
|
|
19
|
-
&& graph.nodes.length <= GRAPH_RENDER_MAX_ROUTED_NODES
|
|
20
|
-
&& graph.edges.length <= GRAPH_RENDER_MAX_ROUTED_EDGES;
|
|
18
|
+
const withinBounds =
|
|
19
|
+
graph.nodes.length > 1 && graph.nodes.length <= GRAPH_RENDER_MAX_ROUTED_NODES && graph.edges.length <= GRAPH_RENDER_MAX_ROUTED_EDGES;
|
|
21
20
|
if (withinBounds) {
|
|
22
21
|
const rendered = renderer.render(graph);
|
|
23
22
|
if (rendered.lines.length > 0) return rendered.lines;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
2
1
|
import {
|
|
2
|
+
type DisplayGraph,
|
|
3
3
|
GRAPH_RENDER_BOX_PADDING,
|
|
4
4
|
GRAPH_RENDER_MAX_FALLBACK_LINES,
|
|
5
5
|
GRAPH_RENDER_MAX_ROUTED_EDGES,
|
|
6
6
|
GRAPH_RENDER_MAX_ROUTED_NODES,
|
|
7
7
|
GRAPH_RENDER_PADDING_X,
|
|
8
8
|
GRAPH_RENDER_PADDING_Y,
|
|
9
|
-
type DisplayGraph,
|
|
10
9
|
type GraphRenderer,
|
|
11
10
|
type RenderedGraph,
|
|
12
11
|
} from "@danypops/papyrus";
|
|
12
|
+
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
13
13
|
|
|
14
14
|
function nodeLabel(label: string): string {
|
|
15
15
|
return label.replace(/\s+/g, " ").trim().replaceAll('"', "'");
|
|
@@ -27,9 +27,7 @@ export function mermaidSource(graph: DisplayGraph): string {
|
|
|
27
27
|
const from = aliases.get(edge.from);
|
|
28
28
|
const to = aliases.get(edge.to);
|
|
29
29
|
if (!from || !to) continue;
|
|
30
|
-
lines.push(edge.label
|
|
31
|
-
? ` ${from} -->|${edgeLabel(edge.label)}| ${to}`
|
|
32
|
-
: ` ${from} --> ${to}`);
|
|
30
|
+
lines.push(edge.label ? ` ${from} -->|${edgeLabel(edge.label)}| ${to}` : ` ${from} --> ${to}`);
|
|
33
31
|
}
|
|
34
32
|
return lines.join("\n");
|
|
35
33
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
3
|
import type { ContextSegmentItem } from "@danypops/jittor";
|
|
4
|
-
import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, CONTEXT_TREE_MAX_NODES, type
|
|
5
|
-
import { discoverSkillDirectories, scanSkillCatalogFootprint, type SkillCatalogFootprint } from "./skill-catalog-footprint.ts";
|
|
4
|
+
import { type Artifact, CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, CONTEXT_TREE_MAX_NODES, type TaskGraph } from "@danypops/papyrus";
|
|
6
5
|
import { ruleInjectionPreview } from "./rules.ts";
|
|
6
|
+
import { discoverSkillDirectories, type SkillCatalogFootprint, scanSkillCatalogFootprint } from "./skill-catalog-footprint.ts";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Papyrus's own real data for the Context Hub: Rules injection cost, the Task containment tree,
|
|
@@ -94,7 +94,9 @@ interface TaskWalkFrame {
|
|
|
94
94
|
/** Bounded, iterative two-pass walk (an explicit-stack pre-order discovery pass, then a reverse-order construction pass) -- containment depth is not assumed to stay small just because it usually does. */
|
|
95
95
|
export function buildTaskItemTree(graph: TaskGraph): ContextSegmentItem[] {
|
|
96
96
|
const byId = new Map(graph.nodes.map((node) => [node.task.id, node]));
|
|
97
|
-
const openIds = new Set(
|
|
97
|
+
const openIds = new Set(
|
|
98
|
+
graph.nodes.filter((node) => node.task.status !== "done" && node.task.status !== "canceled").map((node) => node.task.id),
|
|
99
|
+
);
|
|
98
100
|
const visited = new Set<string>();
|
|
99
101
|
|
|
100
102
|
const rootIds = [...openIds].filter((id) => {
|
|
@@ -112,7 +114,8 @@ export function buildTaskItemTree(graph: TaskGraph): ContextSegmentItem[] {
|
|
|
112
114
|
const index = order.length;
|
|
113
115
|
order.push(frame);
|
|
114
116
|
const node = byId.get(frame.taskId);
|
|
115
|
-
const children = [...(node?.childIds ?? [])]
|
|
117
|
+
const children = [...(node?.childIds ?? [])]
|
|
118
|
+
.reverse()
|
|
116
119
|
.filter((childId) => openIds.has(childId))
|
|
117
120
|
.map((childId) => ({ taskId: childId, parentIndex: index }));
|
|
118
121
|
stack.push(...children);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextSegment, ContextSegmentItem } from "@danypops/jittor";
|
|
2
|
-
import {
|
|
2
|
+
import { type ContextBudget, sumItemTree } from "./context-budget.ts";
|
|
3
3
|
import type { SkillCatalogFootprint } from "./skill-catalog-footprint.ts";
|
|
4
4
|
|
|
5
5
|
/** Human-readable producer identity on Jittor's Context Hub bus -- distinct from PAPYRUS_CONTEXT_INJECTION_CHANNEL's own opaque per-process producerId, which identifies a specific injection stream rather than "which extension". */
|
|
@@ -13,7 +13,11 @@ export const PAPYRUS_CONTEXT_HUB_PRODUCER_NAME = "papyrus";
|
|
|
13
13
|
* one -- nested as up to three drill-down item groups under a single "papyrus" segment instead,
|
|
14
14
|
* preserving the same per-category fidelity the original local /context breakdown had.
|
|
15
15
|
*/
|
|
16
|
-
export function papyrusContextSegment(
|
|
16
|
+
export function papyrusContextSegment(
|
|
17
|
+
ruleBudget: ContextBudget["rules"],
|
|
18
|
+
taskItems: ContextSegmentItem[],
|
|
19
|
+
skills: SkillCatalogFootprint,
|
|
20
|
+
): ContextSegment {
|
|
17
21
|
const items: ContextSegmentItem[] = [];
|
|
18
22
|
if (ruleBudget.entries.length > 0) {
|
|
19
23
|
items.push({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, PAPYRUS_CONTEXT_INJECTION_SCHEMA
|
|
3
|
-
import { ruleInjectionPreview } from "./rules.ts";
|
|
2
|
+
import { type Artifact, CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, PAPYRUS_CONTEXT_INJECTION_SCHEMA } from "@danypops/papyrus";
|
|
4
3
|
import { playbookInjectionPreview } from "./playbook-bridge.ts";
|
|
4
|
+
import { ruleInjectionPreview } from "./rules.ts";
|
|
5
5
|
|
|
6
6
|
export interface ContextPayloadSize {
|
|
7
7
|
characters: number;
|
|
@@ -127,7 +127,9 @@ function buildItemBlocks(
|
|
|
127
127
|
if (item.option.description && !hideDescriptions) {
|
|
128
128
|
const descriptionPrefix = " ";
|
|
129
129
|
const descriptionLines = wrapText(item.option.description, Math.max(8, normalizedWidth - descriptionPrefix.length));
|
|
130
|
-
descriptionLines.forEach((line) =>
|
|
130
|
+
descriptionLines.forEach((line) => {
|
|
131
|
+
lines.push(padLine(descriptionPrefix, line));
|
|
132
|
+
});
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
return { itemIndex, lines };
|