@danypops/papyrus 0.34.2 → 0.35.0
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/README.md +5 -189
- package/package.json +8 -16
- package/src/artifact-relationship-view.ts +23 -0
- package/src/cli.ts +0 -0
- package/src/index.ts +32 -0
- package/src/task-relationship-view.ts +2 -1
- package/extension/src/active-task-continuation.ts +0 -131
- package/extension/src/artifact-browser.ts +0 -229
- package/extension/src/artifact-detail-format.ts +0 -31
- package/extension/src/artifact-detail-view.ts +0 -112
- package/extension/src/artifact-format.ts +0 -84
- package/extension/src/artifact-status-presentation.ts +0 -71
- package/extension/src/base-prompt-breakdown.ts +0 -55
- package/extension/src/beautiful-mermaid-renderer.ts +0 -68
- package/extension/src/bounded-poll.ts +0 -20
- package/extension/src/context-budget.ts +0 -503
- package/extension/src/context-injection-telemetry.ts +0 -88
- package/extension/src/context-view.ts +0 -222
- package/extension/src/discuss-ask-layout.ts +0 -193
- package/extension/src/discuss-ask-view.ts +0 -1301
- package/extension/src/discuss.ts +0 -134
- package/extension/src/discussion-detail-view.ts +0 -136
- package/extension/src/docs.ts +0 -58
- package/extension/src/domain-tools.ts +0 -886
- package/extension/src/index.ts +0 -776
- package/extension/src/markdown.ts +0 -60
- package/extension/src/note-widget.ts +0 -8
- package/extension/src/notes.ts +0 -102
- package/extension/src/playbook-bridge.ts +0 -91
- package/extension/src/playbooks.ts +0 -97
- package/extension/src/rules.ts +0 -51
- package/extension/src/service-client.ts +0 -29
- package/extension/src/session-identity.ts +0 -22
- package/extension/src/skill-catalog-footprint.ts +0 -183
- package/extension/src/skills.ts +0 -127
- package/extension/src/task-context.ts +0 -1
- package/extension/src/task-detail-format.ts +0 -110
- package/extension/src/task-detail-view.ts +0 -139
- package/extension/src/task-focus-events.ts +0 -57
- package/extension/src/task-graph.ts +0 -116
- package/extension/src/task-presentation.ts +0 -26
- package/extension/src/task-widget.ts +0 -70
- package/extension/src/tasks.ts +0 -418
- package/extension/src/tool-rendering/artifact-card.ts +0 -117
- package/extension/src/tool-rendering/artifact-list.ts +0 -179
- package/extension/src/tool-rendering/index.ts +0 -109
- 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,31 +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): 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: (artifact.edges ?? []).map((edge) => `${edge.from} --${edge.relation}--> ${edge.to}`),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function artifactDetailsText(artifact: Artifact): string {
|
|
25
|
-
const content = artifactDetailContent(artifact);
|
|
26
|
-
let output = `${content.title}\n${content.identity}\n\n${content.body}`;
|
|
27
|
-
if (content.labels.length > 0) output += `\n\nLabels: ${content.labels.join(", ")}`;
|
|
28
|
-
if (content.metadata.length > 0) output += `\n\nMetadata:\n${content.metadata.map((line) => ` ${line}`).join("\n")}`;
|
|
29
|
-
if (content.relationships.length > 0) output += `\n\nRelationships:\n${content.relationships.map((line) => ` ${line}`).join("\n")}`;
|
|
30
|
-
return output;
|
|
31
|
-
}
|
|
@@ -1,112 +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 { artifactDetailContent, artifactDetailsText, type ArtifactDetailContent } from "./artifact-detail-format.ts";
|
|
11
|
-
import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
|
|
12
|
-
|
|
13
|
-
interface ArtifactDetailLine {
|
|
14
|
-
text: string;
|
|
15
|
-
wide: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class ArtifactDetailViewport {
|
|
19
|
-
private offsetX = 0;
|
|
20
|
-
private offsetY = 0;
|
|
21
|
-
private renderedWidth = 0;
|
|
22
|
-
private lines: ArtifactDetailLine[] = [];
|
|
23
|
-
private readonly visibleLines: number;
|
|
24
|
-
private readonly content: ArtifactDetailContent;
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
private readonly tui: TUI,
|
|
28
|
-
private readonly activeTheme: ActiveTheme,
|
|
29
|
-
artifact: Artifact,
|
|
30
|
-
private readonly close: () => void,
|
|
31
|
-
) {
|
|
32
|
-
this.visibleLines = Math.max(
|
|
33
|
-
ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
|
|
34
|
-
Math.min(ARTIFACT_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - ARTIFACT_DETAIL_RESERVED_ROWS),
|
|
35
|
-
);
|
|
36
|
-
this.content = artifactDetailContent(artifact);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
invalidate(): void { this.renderedWidth = 0; }
|
|
40
|
-
|
|
41
|
-
render(width: number): string[] {
|
|
42
|
-
const contentWidth = Math.max(1, width - 2);
|
|
43
|
-
this.buildLines(contentWidth);
|
|
44
|
-
const wideWidth = this.content.relationships.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0);
|
|
45
|
-
this.offsetX = Math.min(this.offsetX, Math.max(0, wideWidth - contentWidth));
|
|
46
|
-
this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
|
|
47
|
-
const end = Math.min(this.lines.length, this.offsetY + this.visibleLines);
|
|
48
|
-
const theme = this.activeTheme();
|
|
49
|
-
const border = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
|
|
50
|
-
const footer = [
|
|
51
|
-
wideWidth > contentWidth ? `←/→ relationships · column ${this.offsetX + 1}/${wideWidth}` : "",
|
|
52
|
-
this.lines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.lines.length}` : "",
|
|
53
|
-
"Esc back",
|
|
54
|
-
].filter(Boolean).join(" · ");
|
|
55
|
-
return [
|
|
56
|
-
border,
|
|
57
|
-
truncateToWidth(theme.fg("accent", theme.bold("Artifact details")), width, ""),
|
|
58
|
-
border,
|
|
59
|
-
...this.lines.slice(this.offsetY, end).map((line) => line.wide
|
|
60
|
-
? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
|
|
61
|
-
: truncateToWidth(` ${line.text}`, width, "")),
|
|
62
|
-
truncateToWidth(theme.fg("dim", footer), width, ""),
|
|
63
|
-
border,
|
|
64
|
-
];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
handleInput(data: string): void {
|
|
68
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
|
|
69
|
-
if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
|
|
70
|
-
else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.lines.length - this.visibleLines), this.offsetY + 1);
|
|
71
|
-
else if (matchesKey(data, "left")) this.offsetX = Math.max(0, this.offsetX - ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS);
|
|
72
|
-
else if (matchesKey(data, "right")) this.offsetX += ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS;
|
|
73
|
-
else return;
|
|
74
|
-
this.tui.requestRender();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private buildLines(width: number): void {
|
|
78
|
-
if (this.renderedWidth === width) return;
|
|
79
|
-
this.renderedWidth = width;
|
|
80
|
-
const theme = this.activeTheme();
|
|
81
|
-
const wrap = (text: string, color: "text" | "muted" | "dim" = "text"): ArtifactDetailLine[] =>
|
|
82
|
-
(text.length === 0 ? [""] : wrapTextWithAnsi(theme.fg(color, text), width)).map((line) => ({ text: line, wide: false }));
|
|
83
|
-
const identity = [
|
|
84
|
-
...wrap(theme.bold(this.content.title)),
|
|
85
|
-
...wrap(this.content.identity, "muted"),
|
|
86
|
-
{ text: "", wide: false },
|
|
87
|
-
];
|
|
88
|
-
const body = renderMarkdownBody(this.content.body, width, this.activeTheme).map((text) => ({ text, wide: false }));
|
|
89
|
-
const labels = this.content.labels.length > 0
|
|
90
|
-
? [{ text: "", wide: false }, ...wrap("Labels:", "muted"), ...wrap(this.content.labels.join(", "))]
|
|
91
|
-
: [];
|
|
92
|
-
const metadata = this.content.metadata.length > 0
|
|
93
|
-
? [{ text: "", wide: false }, ...wrap("Metadata:", "muted"), ...this.content.metadata.flatMap((line) => wrap(` ${line}`, "dim"))]
|
|
94
|
-
: [];
|
|
95
|
-
const relationships = this.content.relationships.length > 0
|
|
96
|
-
? [
|
|
97
|
-
{ text: "", wide: false },
|
|
98
|
-
...wrap("Relationships:", "muted"),
|
|
99
|
-
...this.content.relationships.map((text) => ({ text: theme.fg("text", text), wide: true })),
|
|
100
|
-
]
|
|
101
|
-
: [];
|
|
102
|
-
this.lines = [...identity, ...body, ...labels, ...metadata, ...relationships];
|
|
103
|
-
this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function showArtifactDetailView(ctx: ExtensionCommandContext, artifact: Artifact): Promise<void> {
|
|
108
|
-
const output = artifactDetailsText(artifact);
|
|
109
|
-
if (ctx.mode !== "tui") { ctx.ui.notify(output, "info"); return; }
|
|
110
|
-
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
111
|
-
new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, done));
|
|
112
|
-
}
|
|
@@ -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,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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
2
|
-
import {
|
|
3
|
-
GRAPH_RENDER_BOX_PADDING,
|
|
4
|
-
GRAPH_RENDER_MAX_FALLBACK_LINES,
|
|
5
|
-
GRAPH_RENDER_MAX_ROUTED_EDGES,
|
|
6
|
-
GRAPH_RENDER_MAX_ROUTED_NODES,
|
|
7
|
-
GRAPH_RENDER_PADDING_X,
|
|
8
|
-
GRAPH_RENDER_PADDING_Y,
|
|
9
|
-
} from "../../src/constants.ts";
|
|
10
|
-
import type { DisplayGraph, RenderedGraph } from "../../src/domain/display-graph.ts";
|
|
11
|
-
import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
|
|
12
|
-
|
|
13
|
-
function nodeLabel(label: string): string {
|
|
14
|
-
return label.replace(/\s+/g, " ").trim().replaceAll('"', "'");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function edgeLabel(label: string): string {
|
|
18
|
-
return label.replace(/\s+/g, " ").trim().replaceAll("|", "/");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function mermaidSource(graph: DisplayGraph): string {
|
|
22
|
-
const aliases = new Map(graph.nodes.map((node, index) => [node.id, `n${index}`]));
|
|
23
|
-
const lines = [`flowchart ${graph.direction}`];
|
|
24
|
-
for (const node of graph.nodes) lines.push(` ${aliases.get(node.id)}["${nodeLabel(node.label)}"]`);
|
|
25
|
-
for (const edge of graph.edges) {
|
|
26
|
-
const from = aliases.get(edge.from);
|
|
27
|
-
const to = aliases.get(edge.to);
|
|
28
|
-
if (!from || !to) continue;
|
|
29
|
-
lines.push(edge.label
|
|
30
|
-
? ` ${from} -->|${edgeLabel(edge.label)}| ${to}`
|
|
31
|
-
: ` ${from} --> ${to}`);
|
|
32
|
-
}
|
|
33
|
-
return lines.join("\n");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function boundedLineFallback(graph: DisplayGraph): RenderedGraph {
|
|
37
|
-
const candidates = [
|
|
38
|
-
"┌─ Task graph ─",
|
|
39
|
-
`│ ${graph.nodes.length} nodes · ${graph.edges.length} edges · routed layout skipped above ${GRAPH_RENDER_MAX_ROUTED_NODES} nodes`,
|
|
40
|
-
"├─ Nodes",
|
|
41
|
-
...graph.nodes.map((node) => `│ ${node.label}`),
|
|
42
|
-
"├─ Edges",
|
|
43
|
-
...graph.edges.map((edge) => `│ ${edge.from} ─${edge.label ? `${edge.label}─` : ""}→ ${edge.to}`),
|
|
44
|
-
];
|
|
45
|
-
const contentLimit = Math.max(1, GRAPH_RENDER_MAX_FALLBACK_LINES - 1);
|
|
46
|
-
const lines = candidates.slice(0, contentLimit);
|
|
47
|
-
const omitted = candidates.length - lines.length;
|
|
48
|
-
if (omitted > 0) lines[lines.length - 1] = `│ … ${omitted + 1} lines omitted`;
|
|
49
|
-
lines.push("└─");
|
|
50
|
-
return { lines };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class BeautifulMermaidRenderer implements GraphRenderer {
|
|
54
|
-
render(graph: DisplayGraph): RenderedGraph {
|
|
55
|
-
if (graph.nodes.length === 0) return { lines: [] };
|
|
56
|
-
if (graph.nodes.length > GRAPH_RENDER_MAX_ROUTED_NODES || graph.edges.length > GRAPH_RENDER_MAX_ROUTED_EDGES) {
|
|
57
|
-
return boundedLineFallback(graph);
|
|
58
|
-
}
|
|
59
|
-
const output = renderMermaidASCII(mermaidSource(graph), {
|
|
60
|
-
useAscii: false,
|
|
61
|
-
paddingX: GRAPH_RENDER_PADDING_X,
|
|
62
|
-
paddingY: GRAPH_RENDER_PADDING_Y,
|
|
63
|
-
boxBorderPadding: GRAPH_RENDER_BOX_PADDING,
|
|
64
|
-
colorMode: "none",
|
|
65
|
-
});
|
|
66
|
-
return { lines: output.replace(/\s+$/g, "").split("\n") };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared idempotent start/stop wrapper over setInterval, extracted once TaskOverlay and
|
|
3
|
-
* NoteOverlay both needed the identical "fallback refresh for a mutation no event announces"
|
|
4
|
-
* behavior -- a second start() is a no-op rather than a competing timer, and stop() is safe
|
|
5
|
-
* to call even if never started.
|
|
6
|
-
*/
|
|
7
|
-
export class BoundedPoll {
|
|
8
|
-
private timer: ReturnType<typeof setInterval> | undefined;
|
|
9
|
-
|
|
10
|
-
start(intervalMs: number, tick: () => void): void {
|
|
11
|
-
if (this.timer) return;
|
|
12
|
-
this.timer = setInterval(tick, intervalMs);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
stop(): void {
|
|
16
|
-
if (!this.timer) return;
|
|
17
|
-
clearInterval(this.timer);
|
|
18
|
-
this.timer = undefined;
|
|
19
|
-
}
|
|
20
|
-
}
|