@danypops/papyrus 0.34.3 → 0.35.1

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 (47) hide show
  1. package/README.md +5 -189
  2. package/package.json +8 -16
  3. package/src/cli.ts +0 -0
  4. package/src/index.ts +32 -0
  5. package/src/modules/discuss.ts +6 -1
  6. package/extension/src/active-task-continuation.ts +0 -131
  7. package/extension/src/artifact-browser.ts +0 -229
  8. package/extension/src/artifact-detail-format.ts +0 -38
  9. package/extension/src/artifact-detail-view.ts +0 -121
  10. package/extension/src/artifact-format.ts +0 -84
  11. package/extension/src/artifact-relationship-lines.ts +0 -24
  12. package/extension/src/artifact-status-presentation.ts +0 -71
  13. package/extension/src/base-prompt-breakdown.ts +0 -55
  14. package/extension/src/beautiful-mermaid-renderer.ts +0 -68
  15. package/extension/src/bounded-poll.ts +0 -20
  16. package/extension/src/context-budget.ts +0 -503
  17. package/extension/src/context-injection-telemetry.ts +0 -88
  18. package/extension/src/context-view.ts +0 -222
  19. package/extension/src/discuss-ask-layout.ts +0 -193
  20. package/extension/src/discuss-ask-view.ts +0 -1301
  21. package/extension/src/discuss.ts +0 -134
  22. package/extension/src/discussion-detail-view.ts +0 -136
  23. package/extension/src/docs.ts +0 -58
  24. package/extension/src/domain-tools.ts +0 -886
  25. package/extension/src/index.ts +0 -776
  26. package/extension/src/markdown.ts +0 -60
  27. package/extension/src/note-widget.ts +0 -8
  28. package/extension/src/notes.ts +0 -102
  29. package/extension/src/playbook-bridge.ts +0 -91
  30. package/extension/src/playbooks.ts +0 -97
  31. package/extension/src/rules.ts +0 -51
  32. package/extension/src/service-client.ts +0 -29
  33. package/extension/src/session-identity.ts +0 -22
  34. package/extension/src/skill-catalog-footprint.ts +0 -183
  35. package/extension/src/skills.ts +0 -127
  36. package/extension/src/task-context.ts +0 -1
  37. package/extension/src/task-detail-format.ts +0 -110
  38. package/extension/src/task-detail-view.ts +0 -139
  39. package/extension/src/task-focus-events.ts +0 -57
  40. package/extension/src/task-graph.ts +0 -116
  41. package/extension/src/task-presentation.ts +0 -26
  42. package/extension/src/task-widget.ts +0 -70
  43. package/extension/src/tasks.ts +0 -418
  44. package/extension/src/tool-rendering/artifact-card.ts +0 -117
  45. package/extension/src/tool-rendering/artifact-list.ts +0 -179
  46. package/extension/src/tool-rendering/index.ts +0 -109
  47. package/extension/src/tool-rendering/render-model.ts +0 -410
@@ -1,229 +0,0 @@
1
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
2
- import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
3
- import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
- import { SEED_RELATIONS } from "../../src/constants.ts";
5
- import type { Artifact } from "../../src/domain/artifact.ts";
6
- import type { OperationName } from "../../src/service.ts";
7
- import type { StatusPresentation } from "./artifact-status-presentation.ts";
8
- import { artifactDetailsText } from "./artifact-detail-format.ts";
9
- import { showArtifactDetailView } from "./artifact-detail-view.ts";
10
- import { callService } from "./service-client.ts";
11
-
12
- export { artifactDetailsText } from "./artifact-detail-format.ts";
13
-
14
- const BROWSER_QUERY_LIMIT = 500;
15
- const BROWSER_VISIBLE_ROWS = 20;
16
- const DETAIL_GRAPH_DEPTH = 4;
17
- const DETAIL_GRAPH_NODES = 100;
18
-
19
- export interface ArtifactBrowserConfig {
20
- kind: string;
21
- title: string;
22
- statusOrder: string[];
23
- presentation: Record<string, StatusPresentation>;
24
- listOperation?: OperationName;
25
- listInput?: Record<string, unknown>;
26
- rowMeta(row: Artifact, theme: Theme): string;
27
- actions(row: Artifact): string[];
28
- handleAction(choice: string, row: Artifact, ctx: ExtensionCommandContext): Promise<void>;
29
- }
30
-
31
- export function filterArtifactRows(rows: Artifact[], query: string): Artifact[] {
32
- const needle = query.trim().toLowerCase();
33
- if (!needle) return [...rows];
34
- return rows.filter((row) => [
35
- row.id,
36
- row.title,
37
- row.body,
38
- row.subtype,
39
- row.labels.join(" "),
40
- JSON.stringify(row.extra),
41
- ].some((value) => value.toLowerCase().includes(needle)));
42
- }
43
-
44
- export function statusSummary(rows: Artifact[], order: string[]): Array<{ status: string; count: number }> {
45
- const counts = new Map<string, number>();
46
- for (const row of rows) counts.set(row.status, (counts.get(row.status) ?? 0) + 1);
47
- return order.filter((status) => counts.has(status)).map((status) => ({ status, count: counts.get(status)! }));
48
- }
49
-
50
- async function loadArtifacts(config: ArtifactBrowserConfig): Promise<Artifact[]> {
51
- return callService<Record<string, unknown>, Artifact[]>(config.listOperation ?? "artifact.query", {
52
- kind: config.kind,
53
- limit: BROWSER_QUERY_LIMIT,
54
- ...(config.listInput ?? {}),
55
- });
56
- }
57
-
58
- export type ArtifactDetailLoader = (
59
- operation: OperationName,
60
- input: Record<string, unknown>,
61
- ) => Promise<Artifact | null>;
62
-
63
- const loadArtifactDetails: ArtifactDetailLoader = (operation, input) =>
64
- callService<Record<string, unknown>, Artifact | null>(operation, input);
65
-
66
- export async function showArtifactDetails(
67
- ctx: ExtensionCommandContext,
68
- id: string,
69
- operation: OperationName = "artifact.show",
70
- input: Record<string, unknown> = {},
71
- load: ArtifactDetailLoader = loadArtifactDetails,
72
- ): Promise<void> {
73
- try {
74
- const artifact = await load(operation, {
75
- id,
76
- ...input,
77
- tree: true,
78
- depth: DETAIL_GRAPH_DEPTH,
79
- max_nodes: DETAIL_GRAPH_NODES,
80
- });
81
- if (!artifact) { ctx.ui.notify("Artifact not found", "error"); return; }
82
- await showArtifactDetailView(ctx, artifact);
83
- } catch (error) {
84
- ctx.ui.notify(`Show details failed: ${error instanceof Error ? error.message : error}`, "error");
85
- }
86
- }
87
-
88
- export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: string, fixedRelation?: string): Promise<void> {
89
- const target = await ctx.ui.input("Target artifact id:", "");
90
- if (!target) return;
91
- const relation = fixedRelation ?? await ctx.ui.select("Relation", [...SEED_RELATIONS]);
92
- if (!relation) return;
93
- try {
94
- await callService("graph.link", { from: fromId, relation, to: target });
95
- ctx.ui.notify(`Artifacts linked via ${relation}`, "info");
96
- } catch (error) {
97
- ctx.ui.notify(`Link failed: ${error instanceof Error ? error.message : error}`, "error");
98
- }
99
- }
100
-
101
- export async function setArtifactStatus(ctx: ExtensionCommandContext, id: string, status: string): Promise<void> {
102
- try {
103
- const artifact = await callService<Record<string, unknown>, Artifact | null>("graph.status", { id, status });
104
- if (!artifact) { ctx.ui.notify("Artifact not found", "error"); return; }
105
- ctx.ui.notify(`${artifact.title} → [${artifact.status}]`, "info");
106
- } catch (error) {
107
- ctx.ui.notify(`Status change failed: ${error instanceof Error ? error.message : error}`, "error");
108
- }
109
- }
110
-
111
- export async function showArtifactBrowser(ctx: ExtensionCommandContext, config: ArtifactBrowserConfig): Promise<void> {
112
- if (!ctx.hasUI) {
113
- ctx.ui.notify(`/${config.kind}s requires interactive mode`, "warning");
114
- return;
115
- }
116
- let rows = await loadArtifacts(config);
117
- if (rows.length === 0) {
118
- ctx.ui.notify(`No ${config.kind} artifacts yet. Ask the agent to create one.`, "info");
119
- return;
120
- }
121
-
122
- for (;;) {
123
- const selected = await renderPanel(ctx, rows, config);
124
- if (selected === undefined) return;
125
- if (selected === "refresh") { rows = await loadArtifacts(config); continue; }
126
- const choices = config.actions(selected);
127
- const choice = await ctx.ui.select(selected.title, choices);
128
- if (!choice) continue;
129
- await config.handleAction(choice, selected, ctx);
130
- rows = await loadArtifacts(config);
131
- }
132
- }
133
-
134
- function renderPanel(
135
- ctx: ExtensionCommandContext,
136
- rows: Artifact[],
137
- config: ArtifactBrowserConfig,
138
- ): Promise<Artifact | "refresh" | undefined> {
139
- return ctx.ui.custom<Artifact | "refresh" | undefined>((tui, theme, _keybindings, done) => {
140
- const input = new Input();
141
- let searchActive = false;
142
- let filtered = [...rows];
143
- let selectedIndex = 0;
144
-
145
- function applyFilter(): void {
146
- filtered = filterArtifactRows(rows, input.getValue());
147
- selectedIndex = 0;
148
- }
149
-
150
- const header = {
151
- invalidate() {},
152
- render(width: number): string[] {
153
- const title = theme.bold(config.title);
154
- const hint = searchActive
155
- ? rawKeyHint("esc", "clear")
156
- : [rawKeyHint("enter", "actions"), rawKeyHint("/", "filter"), rawKeyHint("r", "refresh"), rawKeyHint("esc", "close")]
157
- .join(theme.fg("muted", " · "));
158
- const spacing = Math.max(1, width - visibleWidth(title) - visibleWidth(hint));
159
- const summary = statusSummary(rows, config.statusOrder)
160
- .map(({ status, count }) => {
161
- const presentation = config.presentation[status];
162
- const glyph = presentation ? theme.fg(presentation.color, presentation.glyph) : status;
163
- return `${glyph} ${count} ${status}`;
164
- })
165
- .join(", ");
166
- return [
167
- truncateToWidth(`${title}${" ".repeat(spacing)}${hint}`, width, ""),
168
- truncateToWidth(theme.fg("muted", summary), width, ""),
169
- ];
170
- },
171
- };
172
-
173
- const list = {
174
- invalidate() {},
175
- render(width: number): string[] {
176
- const lines = searchActive ? [...input.render(width), ""] : [""];
177
- if (filtered.length === 0) return [...lines, theme.fg("muted", ` No matching ${config.kind}s`)];
178
- const start = Math.max(0, Math.min(selectedIndex - Math.floor(BROWSER_VISIBLE_ROWS / 2), filtered.length - BROWSER_VISIBLE_ROWS));
179
- const end = Math.min(start + BROWSER_VISIBLE_ROWS, filtered.length);
180
- for (let index = start; index < end; index++) {
181
- const row = filtered[index]!;
182
- const selected = index === selectedIndex;
183
- const cursor = selected ? theme.fg("accent", "❯") : " ";
184
- const presentation = config.presentation[row.status];
185
- const glyph = presentation ? theme.fg(presentation.color, presentation.glyph) : "?";
186
- const title = selected ? theme.bold(row.title) : row.title;
187
- const meta = config.rowMeta(row, theme);
188
- lines.push(truncateToWidth(`${cursor} ${glyph} ${title}${meta ? `${theme.fg("dim", " · ")}${meta}` : ""}`, width, ""));
189
- }
190
- lines.push(theme.fg("muted", ` ${selectedIndex + 1}/${filtered.length} ${config.kind}`));
191
- return lines;
192
- },
193
- };
194
-
195
- const container = new Container();
196
- container.addChild(new Spacer(1));
197
- container.addChild(new DynamicBorder());
198
- container.addChild(new Spacer(1));
199
- container.addChild(header);
200
- container.addChild(new Spacer(1));
201
- container.addChild(list);
202
- container.addChild(new Spacer(1));
203
- container.addChild(new DynamicBorder());
204
-
205
- return {
206
- render: (width: number) => container.render(width),
207
- invalidate: () => container.invalidate(),
208
- handleInput(data: string) {
209
- if (searchActive) {
210
- if (data === "\x1b") { searchActive = false; applyFilter(); }
211
- else if (data === "\r") searchActive = false;
212
- else { input.handleInput(data); applyFilter(); }
213
- tui.requestRender();
214
- return;
215
- }
216
- switch (data) {
217
- case "\x1b[A": selectedIndex = (selectedIndex - 1 + filtered.length) % Math.max(filtered.length, 1); break;
218
- case "\x1b[B": selectedIndex = (selectedIndex + 1) % Math.max(filtered.length, 1); break;
219
- case "/": searchActive = true; break;
220
- case "r": done("refresh"); return;
221
- case "\r": { const row = filtered[selectedIndex]; if (row) done(row); return; }
222
- case "\x1b": done(undefined); return;
223
- default: return;
224
- }
225
- tui.requestRender();
226
- },
227
- };
228
- });
229
- }
@@ -1,38 +0,0 @@
1
- import type { Artifact } from "../../src/domain/artifact.ts";
2
- import { formatMetadata } from "./artifact-format.ts";
3
-
4
- export interface ArtifactDetailContent {
5
- title: string;
6
- identity: string;
7
- body: string;
8
- labels: string[];
9
- metadata: string[];
10
- relationships: string[];
11
- }
12
-
13
- export function artifactDetailContent(artifact: Artifact, relationshipLines: string[] = []): ArtifactDetailContent {
14
- return {
15
- title: artifact.title,
16
- identity: `${artifact.id} [${artifact.kind}|${artifact.status}]${artifact.subtype ? ` · ${artifact.subtype}` : ""}`,
17
- body: artifact.body || "(no body)",
18
- labels: [...artifact.labels],
19
- metadata: Object.keys(artifact.extra).length > 0 ? formatMetadata(artifact.extra) : [],
20
- relationships: relationshipLines,
21
- };
22
- }
23
-
24
- /**
25
- * Deliberately plain, unlike ArtifactDetailViewport's TUI body (renderMarkdownBody): this path
26
- * feeds ctx.ui.notify(), used outside interactive mode (RPC, piped/non-terminal callers) where
27
- * ANSI escape codes are noise or corruption, not formatting. Raw Markdown source stays readable
28
- * as plain text either way. relationshipLines is still upgraded (a real graph or a resolved
29
- * arrow list, never raw ids) since that needs no color to read.
30
- */
31
- export function artifactDetailsText(artifact: Artifact, relationshipLines: string[] = []): string {
32
- const content = artifactDetailContent(artifact, relationshipLines);
33
- let output = `${content.title}\n${content.identity}\n\n${content.body}`;
34
- if (content.labels.length > 0) output += `\n\nLabels: ${content.labels.join(", ")}`;
35
- if (content.metadata.length > 0) output += `\n\nMetadata:\n${content.metadata.map((line) => ` ${line}`).join("\n")}`;
36
- if (content.relationships.length > 0) output += `\n\nRelationships:\n${content.relationships.map((line) => ` ${line}`).join("\n")}`;
37
- return output;
38
- }
@@ -1,121 +0,0 @@
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 {
4
- ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS,
5
- ARTIFACT_DETAIL_MAX_VISIBLE_LINES,
6
- ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
7
- ARTIFACT_DETAIL_RESERVED_ROWS,
8
- } from "../../src/constants.ts";
9
- import type { Artifact } from "../../src/domain/artifact.ts";
10
- import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
11
- import { artifactDetailContent, artifactDetailsText, type ArtifactDetailContent } from "./artifact-detail-format.ts";
12
- import { buildArtifactRelationshipLines } from "./artifact-relationship-lines.ts";
13
- import { BeautifulMermaidRenderer } from "./beautiful-mermaid-renderer.ts";
14
- import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
15
-
16
- interface ArtifactDetailLine {
17
- text: string;
18
- wide: boolean;
19
- }
20
-
21
- class ArtifactDetailViewport {
22
- private offsetX = 0;
23
- private offsetY = 0;
24
- private renderedWidth = 0;
25
- private lines: ArtifactDetailLine[] = [];
26
- private readonly visibleLines: number;
27
- private readonly content: ArtifactDetailContent;
28
-
29
- constructor(
30
- private readonly tui: TUI,
31
- private readonly activeTheme: ActiveTheme,
32
- artifact: Artifact,
33
- relationshipLines: string[],
34
- private readonly close: () => void,
35
- ) {
36
- this.visibleLines = Math.max(
37
- ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
38
- Math.min(ARTIFACT_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - ARTIFACT_DETAIL_RESERVED_ROWS),
39
- );
40
- this.content = artifactDetailContent(artifact, relationshipLines);
41
- }
42
-
43
- invalidate(): void { this.renderedWidth = 0; }
44
-
45
- render(width: number): string[] {
46
- const contentWidth = Math.max(1, width - 2);
47
- this.buildLines(contentWidth);
48
- const wideWidth = this.content.relationships.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0);
49
- this.offsetX = Math.min(this.offsetX, Math.max(0, wideWidth - contentWidth));
50
- this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
51
- const end = Math.min(this.lines.length, this.offsetY + this.visibleLines);
52
- const theme = this.activeTheme();
53
- const border = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
54
- const footer = [
55
- wideWidth > contentWidth ? `←/→ relationships · column ${this.offsetX + 1}/${wideWidth}` : "",
56
- this.lines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.lines.length}` : "",
57
- "Esc back",
58
- ].filter(Boolean).join(" · ");
59
- return [
60
- border,
61
- truncateToWidth(theme.fg("accent", theme.bold("Artifact details")), width, ""),
62
- border,
63
- ...this.lines.slice(this.offsetY, end).map((line) => line.wide
64
- ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
65
- : truncateToWidth(` ${line.text}`, width, "")),
66
- truncateToWidth(theme.fg("dim", footer), width, ""),
67
- border,
68
- ];
69
- }
70
-
71
- handleInput(data: string): void {
72
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
73
- if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
74
- else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.lines.length - this.visibleLines), this.offsetY + 1);
75
- else if (matchesKey(data, "left")) this.offsetX = Math.max(0, this.offsetX - ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS);
76
- else if (matchesKey(data, "right")) this.offsetX += ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS;
77
- else return;
78
- this.tui.requestRender();
79
- }
80
-
81
- private buildLines(width: number): void {
82
- if (this.renderedWidth === width) return;
83
- this.renderedWidth = width;
84
- const theme = this.activeTheme();
85
- const wrap = (text: string, color: "text" | "muted" | "dim" = "text"): ArtifactDetailLine[] =>
86
- (text.length === 0 ? [""] : wrapTextWithAnsi(theme.fg(color, text), width)).map((line) => ({ text: line, wide: false }));
87
- const identity = [
88
- ...wrap(theme.bold(this.content.title)),
89
- ...wrap(this.content.identity, "muted"),
90
- { text: "", wide: false },
91
- ];
92
- const body = renderMarkdownBody(this.content.body, width, this.activeTheme).map((text) => ({ text, wide: false }));
93
- const labels = this.content.labels.length > 0
94
- ? [{ text: "", wide: false }, ...wrap("Labels:", "muted"), ...wrap(this.content.labels.join(", "))]
95
- : [];
96
- const metadata = this.content.metadata.length > 0
97
- ? [{ text: "", wide: false }, ...wrap("Metadata:", "muted"), ...this.content.metadata.flatMap((line) => wrap(` ${line}`, "dim"))]
98
- : [];
99
- const relationships = this.content.relationships.length > 0
100
- ? [
101
- { text: "", wide: false },
102
- ...wrap("Relationships:", "muted"),
103
- ...this.content.relationships.map((text) => ({ text: theme.fg("text", text), wide: true })),
104
- ]
105
- : [];
106
- this.lines = [...identity, ...body, ...labels, ...metadata, ...relationships];
107
- this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
108
- }
109
- }
110
-
111
- export async function showArtifactDetailView(
112
- ctx: ExtensionCommandContext,
113
- artifact: Artifact,
114
- renderer: GraphRenderer = new BeautifulMermaidRenderer(),
115
- ): Promise<void> {
116
- const relationshipLines = buildArtifactRelationshipLines(artifact, renderer);
117
- const output = artifactDetailsText(artifact, relationshipLines);
118
- if (ctx.mode !== "tui") { ctx.ui.notify(output, "info"); return; }
119
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
120
- new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, relationshipLines, done));
121
- }
@@ -1,84 +0,0 @@
1
- import {
2
- DEFAULT_METADATA_DEPTH,
3
- DEFAULT_METADATA_ITEMS,
4
- MAX_METADATA_DEPTH,
5
- MAX_METADATA_ITEMS,
6
- } from "../../src/constants.ts";
7
-
8
- const STATUS_GLYPHS: Record<string, string> = {
9
- todo: "○",
10
- "in-progress": "●",
11
- review: "◆",
12
- rejected: "▲",
13
- done: "■",
14
- canceled: "×",
15
- };
16
-
17
- export interface MetadataFormatOptions {
18
- maxDepth?: number;
19
- maxItems?: number;
20
- }
21
-
22
- function isRecord(value: unknown): value is Record<string, unknown> {
23
- return typeof value === "object" && value !== null && !Array.isArray(value);
24
- }
25
-
26
- function scalar(value: unknown): string {
27
- if (typeof value === "string") return value;
28
- if (value === null) return "null";
29
- if (value === undefined) return "undefined";
30
- return JSON.stringify(value);
31
- }
32
-
33
- /** Render arbitrary nested artifact metadata into bounded, human-readable lines. */
34
- export function formatMetadata(value: unknown, options: MetadataFormatOptions = {}): string[] {
35
- const maxDepth = Math.min(MAX_METADATA_DEPTH, Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_METADATA_DEPTH)));
36
- const maxItems = Math.min(MAX_METADATA_ITEMS, Math.max(1, Math.floor(options.maxItems ?? DEFAULT_METADATA_ITEMS)));
37
- let renderedItems = 0;
38
-
39
- function render(current: unknown, indent: number, depth: number): string[] {
40
- const pad = " ".repeat(indent);
41
- if (renderedItems >= maxItems) return [`${pad}…`];
42
- if ((Array.isArray(current) || isRecord(current)) && depth >= maxDepth) return [`${pad}…`];
43
-
44
- if (Array.isArray(current)) {
45
- const lines: string[] = [];
46
- for (const item of current) {
47
- if (renderedItems >= maxItems) { lines.push(`${pad}…`); break; }
48
- renderedItems++;
49
- if (isRecord(item) && typeof item["title"] === "string") {
50
- const status = typeof item["status"] === "string" ? item["status"] : "";
51
- const glyph = STATUS_GLYPHS[status];
52
- lines.push(`${pad}- ${glyph ? `${glyph} ` : ""}${item["title"]}`);
53
- const rest = Object.fromEntries(Object.entries(item).filter(([key]) => key !== "title" && key !== "status"));
54
- if (Object.keys(rest).length > 0) lines.push(...render(rest, indent + 1, depth + 1));
55
- } else if (Array.isArray(item) || isRecord(item)) {
56
- lines.push(`${pad}-`);
57
- lines.push(...render(item, indent + 1, depth + 1));
58
- } else {
59
- lines.push(`${pad}- ${scalar(item)}`);
60
- }
61
- }
62
- return lines;
63
- }
64
-
65
- if (isRecord(current)) {
66
- const lines: string[] = [];
67
- for (const [key, item] of Object.entries(current)) {
68
- if (renderedItems >= maxItems) { lines.push(`${pad}…`); break; }
69
- renderedItems++;
70
- if (Array.isArray(item) || isRecord(item)) {
71
- lines.push(`${pad}${key}:`);
72
- lines.push(...render(item, indent + 1, depth + 1));
73
- } else {
74
- lines.push(`${pad}${key}: ${scalar(item)}`);
75
- }
76
- }
77
- return lines;
78
- }
79
-
80
- return [`${pad}${scalar(current)}`];
81
- }
82
-
83
- return render(value, 0, 0);
84
- }
@@ -1,24 +0,0 @@
1
- import { GRAPH_RENDER_MAX_ROUTED_EDGES, GRAPH_RENDER_MAX_ROUTED_NODES } from "../../src/constants.ts";
2
- import { projectArtifactRelationships } from "../../src/artifact-relationship-view.ts";
3
- import type { Artifact } from "../../src/domain/artifact.ts";
4
- import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
5
-
6
- /**
7
- * Renders an artifact's direct relationships as a small graph when the neighbor set is real
8
- * (more than one node) and within the same bound Task graph rendering already enforces;
9
- * otherwise falls back to a plain, still name-resolved arrow list -- never the renderer's own
10
- * separate box-style fallback, so this view has exactly two shapes, not three.
11
- */
12
- export function buildArtifactRelationshipLines(artifact: Artifact, renderer: GraphRenderer): string[] {
13
- const graph = projectArtifactRelationships(artifact);
14
- if (graph.edges.length === 0) return [];
15
- const withinBounds = graph.nodes.length > 1
16
- && graph.nodes.length <= GRAPH_RENDER_MAX_ROUTED_NODES
17
- && graph.edges.length <= GRAPH_RENDER_MAX_ROUTED_EDGES;
18
- if (withinBounds) {
19
- const rendered = renderer.render(graph);
20
- if (rendered.lines.length > 0) return rendered.lines;
21
- }
22
- const labelById = new Map(graph.nodes.map((node) => [node.id, node.label]));
23
- return graph.edges.map((edge) => `${labelById.get(edge.from) ?? edge.from} --${edge.label}--> ${labelById.get(edge.to) ?? edge.to}`);
24
- }
@@ -1,71 +0,0 @@
1
- import type { ThemeColor } from "@earendil-works/pi-coding-agent";
2
-
3
- /**
4
- * Shared {label, glyph, color} shape, mirroring task-presentation.ts's TASK_STATUS_PRESENTATION
5
- * for every other artifact kind's status. Centralizing this closes a real gap: every artifact
6
- * browser (Rules, Docs, Notes, Skills) previously rendered status as a bare glyph with no color at
7
- * all, which is exactly why "hard to understand which rules are active" was a real complaint --
8
- * an active rule's "●" and a deprecated rule's "○" differ only by one filled-vs-hollow pixel shape,
9
- * easy to miss at a glance across a scrolling list.
10
- */
11
- export interface StatusPresentation {
12
- label: string;
13
- glyph: string;
14
- color: ThemeColor;
15
- }
16
-
17
- export const RULE_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
18
- active: { label: "active", glyph: "●", color: "success" },
19
- deprecated: { label: "deprecated", glyph: "○", color: "muted" },
20
- };
21
-
22
- export const DOC_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
23
- draft: { label: "draft", glyph: "○", color: "muted" },
24
- active: { label: "active", glyph: "●", color: "success" },
25
- archived: { label: "archived", glyph: "■", color: "dim" },
26
- };
27
-
28
- export const NOTE_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
29
- draft: { label: "draft", glyph: "○", color: "muted" },
30
- active: { label: "active", glyph: "●", color: "success" },
31
- archived: { label: "archived", glyph: "■", color: "dim" },
32
- };
33
-
34
- export const SKILL_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
35
- active: { label: "active", glyph: "●", color: "success" },
36
- deprecated: { label: "deprecated", glyph: "○", color: "muted" },
37
- };
38
-
39
- export const PLAYBOOK_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
40
- active: { label: "active", glyph: "●", color: "success" },
41
- deprecated: { label: "deprecated", glyph: "○", color: "muted" },
42
- };
43
-
44
- /**
45
- * Keyed by extra.discussion.state, not the shared Doc status column -- a settled Discussion's
46
- * doc.status becomes "archived", but a deferred one stays "active" at the doc level (see
47
- * domain/discussion.ts's header comment). Reusing DOC_STATUS_PRESENTATION here would render
48
- * "deferred" and "active" Discussions with the identical glyph, silently losing the one piece
49
- * of state this feature exists to distinguish.
50
- */
51
- export const DISCUSSION_STATE_PRESENTATION: Record<string, StatusPresentation> = {
52
- active: { label: "active", glyph: "●", color: "accent" },
53
- deferred: { label: "deferred", glyph: "⏸", color: "warning" },
54
- settled: { label: "settled", glyph: "✓", color: "success" },
55
- };
56
-
57
- /** Rule severity gets its own color independent of status -- block is the loudest, info the quietest. */
58
- export const RULE_SEVERITY_PRESENTATION: Record<string, ThemeColor> = {
59
- block: "error",
60
- warn: "warning",
61
- info: "accent",
62
- };
63
-
64
- export function severityColor(severity: string): ThemeColor {
65
- return RULE_SEVERITY_PRESENTATION[severity.toLowerCase()] ?? "muted";
66
- }
67
-
68
- /** Plain glyph lookup, for callers that build uncolored text first and colorize it later (e.g. task-graph's colorizeTaskGraphLine pattern). */
69
- export function glyphOf(presentation: Record<string, StatusPresentation>, status: string): string {
70
- return presentation[status]?.glyph ?? "?";
71
- }
@@ -1,55 +0,0 @@
1
- import type { BuildSystemPromptOptions } from "@earendil-works/pi-coding-agent";
2
- import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN } from "../../src/constants.ts";
3
- import type { ContextSegmentItem } from "./context-budget.ts";
4
-
5
- /**
6
- * Splits Pi's base system prompt into real structural sub-segments instead of one opaque
7
- * number, using BeforeAgentStartEvent's own systemPromptOptions field -- Pi's own doc comment
8
- * on it: "Extensions can inspect this to understand what Pi loaded without re-discovering
9
- * resources." No new hook, no new risk: before_agent_start is already wired.
10
- *
11
- * Deliberately measures each INPUT's raw content size (tool snippet text, skill metadata,
12
- * context file content) rather than attempting to byte-for-byte reproduce Pi's internal
13
- * wrapping/tag format -- buildSystemPrompt() and formatSkillsForPrompt() are Pi-internal
14
- * functions, not part of the public extension API Papyrus can call, so reproducing their
15
- * exact template text here would be a real, silent drift risk if Pi ever changes it. The
16
- * remainder item absorbs whatever wrapping/template text this doesn't attribute, so the
17
- * segment's total always still matches the real observed prompt length exactly -- honesty
18
- * preserved even though individual sub-segment sizes are approximate, matching the same
19
- * known-segments-plus-honest-remainder pattern used everywhere else in this breakdown.
20
- */
21
- export function buildBasePromptItems(options: BuildSystemPromptOptions, totalCharacters: number): ContextSegmentItem[] {
22
- const items: ContextSegmentItem[] = [];
23
-
24
- const toolSnippetEntries = Object.entries(options.toolSnippets ?? {});
25
- // Mirrors buildSystemPrompt()'s own "- name: snippet\n" line shape closely enough to be a
26
- // fair estimate without importing Pi-internal formatting code.
27
- const toolSnippetsCharacters = toolSnippetEntries.reduce((sum, [name, snippet]) => sum + name.length + snippet.length + 4, 0);
28
- if (toolSnippetsCharacters > 0) {
29
- items.push({ label: `Tool snippets (${toolSnippetEntries.length} tools)`, estimatedTokens: toCeilTokens(toolSnippetsCharacters) });
30
- }
31
-
32
- const visibleSkills = (options.skills ?? []).filter((skill) => !skill.disableModelInvocation);
33
- const skillsCharacters = visibleSkills.reduce((sum, skill) => sum + skill.name.length + skill.description.length + skill.filePath.length + 20, 0);
34
- if (skillsCharacters > 0) {
35
- items.push({ label: `Skills catalog (${visibleSkills.length} skills)`, estimatedTokens: toCeilTokens(skillsCharacters) });
36
- }
37
-
38
- const contextFiles = options.contextFiles ?? [];
39
- const contextFilesCharacters = contextFiles.reduce((sum, file) => sum + file.path.length + file.content.length + 40, 0);
40
- if (contextFilesCharacters > 0) {
41
- items.push({ label: `Project context files (${contextFiles.length}, e.g. AGENTS.md)`, estimatedTokens: toCeilTokens(contextFilesCharacters) });
42
- }
43
-
44
- const knownCharacters = toolSnippetsCharacters + skillsCharacters + contextFilesCharacters;
45
- const remainderCharacters = Math.max(0, totalCharacters - knownCharacters);
46
- if (remainderCharacters > 0 || items.length === 0) {
47
- items.push({ label: "Base template, guidelines, and formatting", estimatedTokens: toCeilTokens(remainderCharacters) });
48
- }
49
-
50
- return items;
51
- }
52
-
53
- function toCeilTokens(characters: number): number {
54
- return Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
55
- }