@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.
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 +1 -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 +69 -55
  35. package/extension/src/vehicle-artifact-renderers.ts +58 -0
  36. package/extension/src/vehicle-notes-client.ts +35 -7
  37. 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
- row.title,
35
- row.body,
36
- row.subtype,
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) { ctx.ui.notify("Artifact not found", "error"); return; }
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) { ctx.ui.notify("Artifact not found", "error"); return; }
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") { rows = await loadArtifacts(config); continue; }
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
- .join(theme.fg("muted", " · "));
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") { searchActive = false; applyFilter(); }
209
- else if (data === "\r") searchActive = false;
210
- else { input.handleInput(data); applyFilter(); }
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": selectedIndex = (selectedIndex - 1 + filtered.length) % Math.max(filtered.length, 1); break;
216
- case "\x1b[B": selectedIndex = (selectedIndex + 1) % Math.max(filtered.length, 1); break;
217
- case "/": searchActive = true; break;
218
- case "r": done("refresh"); return;
219
- case "\r": { const row = filtered[selectedIndex]; if (row) done(row); return; }
220
- case "\x1b": done(undefined); return;
221
- default: return;
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 { artifactDetailContent, artifactDetailsText, type ArtifactDetailContent } from "./artifact-detail-format.ts";
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 { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
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 { this.renderedWidth = 0; }
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
- ].filter(Boolean).join(" · ");
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.slice(this.offsetY, end).map((line) => line.wide
65
- ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
66
- : truncateToWidth(` ${line.text}`, width, "")),
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")) { this.close(); return; }
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[] = this.content.metadata.length > 0
102
- ? [{ heading: "Metadata:", lines: this.content.metadata.map((line) => ` ${line}`) }]
103
- : [];
104
- const labelsAndMetadata = (fields.length > 0 || sections.length > 0)
105
- ? buildDetailLines(width, {
106
- fields,
107
- sections,
108
- theme: {
109
- field: (s) => theme.fg("muted", s),
110
- heading: (s) => theme.fg("muted", s),
111
- byline: (s) => theme.fg("dim", s),
112
- body: (s) => theme.fg("text", s),
113
- line: (s) => theme.fg("dim", s),
114
- },
115
- }).map((text) => ({ text, wide: false }))
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 = this.content.relationships.length > 0
123
- ? [
124
- { text: "", wide: false },
125
- ...wrap("Relationships:", "muted"),
126
- ...this.content.relationships.map((text) => ({ text: theme.fg("text", text), wide: true })),
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") { ctx.ui.notify(output, "info"); return; }
142
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
143
- new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, relationshipLines, done));
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) { lines.push(`${pad}…`); break; }
42
+ if (renderedItems >= maxItems) {
43
+ lines.push(`${pad}…`);
44
+ break;
45
+ }
43
46
  renderedItems++;
44
- if (isRecord(item) && typeof item["title"] === "string") {
45
- const status = typeof item["status"] === "string" ? item["status"] : "";
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["title"]}`);
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) { lines.push(`${pad}…`); break; }
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 = graph.nodes.length > 1
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 Artifact, type TaskGraph } from "@danypops/papyrus";
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(graph.nodes.filter((node) => node.task.status !== "done" && node.task.status !== "canceled").map((node) => node.task.id));
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 ?? [])].reverse()
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 { sumItemTree, type ContextBudget } from "./context-budget.ts";
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(ruleBudget: ContextBudget["rules"], taskItems: ContextSegmentItem[], skills: SkillCatalogFootprint): ContextSegment {
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, type Artifact } from "@danypops/papyrus";
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) => lines.push(padLine(descriptionPrefix, line)));
130
+ descriptionLines.forEach((line) => {
131
+ lines.push(padLine(descriptionPrefix, line));
132
+ });
131
133
  }
132
134
 
133
135
  return { itemIndex, lines };