Run prettier on .pi folder

This commit is contained in:
Claudio Ortolina
2026-05-04 21:21:10 +01:00
parent 286d6fecf3
commit 7bf89d9a1d
8 changed files with 272 additions and 191 deletions
+12 -4
View File
@@ -35,7 +35,7 @@ export default function (pi: ExtensionAPI) {
// Proactively tell the agent which paths are off-limits so it doesn't try in the first place // Proactively tell the agent which paths are off-limits so it doesn't try in the first place
pi.on("before_agent_start", async (event, _ctx) => { pi.on("before_agent_start", async (event, _ctx) => {
if (config.blocked_paths.length === 0) return; if (config.blocked_paths.length === 0) return;
const pathList = config.blocked_paths.map((p) => ` - \`${p}\``).join('\n'); const pathList = config.blocked_paths.map((p) => ` - \`${p}\``).join("\n");
return { return {
systemPrompt: systemPrompt:
event.systemPrompt + event.systemPrompt +
@@ -63,7 +63,8 @@ export default function (pi: ExtensionAPI) {
`🚫 ACCESS DENIED by sensitive-file-guard: paths matching "${match}" are permanently protected. ` + `🚫 ACCESS DENIED by sensitive-file-guard: paths matching "${match}" are permanently protected. ` +
`DO NOT retry with other tools (bash, find, ls, grep, etc.). ` + `DO NOT retry with other tools (bash, find, ls, grep, etc.). ` +
`Tell the user the file is protected and STOP.`; `Tell the user the file is protected and STOP.`;
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive path: ${path}`, "warning"); if (ctx.hasUI)
ctx.ui.notify(`Blocked sensitive path: ${path}`, "warning");
return { block: true, reason }; return { block: true, reason };
} }
return; return;
@@ -79,14 +80,21 @@ export default function (pi: ExtensionAPI) {
// Rough substring match: does the pattern text (with glob stripped) // Rough substring match: does the pattern text (with glob stripped)
// appear in the command? // appear in the command?
const literal = p.replace(/[*?]/g, ""); const literal = p.replace(/[*?]/g, "");
return literal.length > 0 && command.toLowerCase().includes(literal.toLowerCase()); return (
literal.length > 0 &&
command.toLowerCase().includes(literal.toLowerCase())
);
}); });
if (pathHit) { if (pathHit) {
const reason = const reason =
`🚫 ACCESS DENIED by sensitive-file-guard: commands targeting "${pathHit}" are permanently blocked. ` + `🚫 ACCESS DENIED by sensitive-file-guard: commands targeting "${pathHit}" are permanently blocked. ` +
`DO NOT retry with alternative commands. Tell the user the path is protected and STOP.`; `DO NOT retry with alternative commands. Tell the user the path is protected and STOP.`;
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive path in bash: ${pathHit}`, "warning"); if (ctx.hasUI)
ctx.ui.notify(
`Blocked sensitive path in bash: ${pathHit}`,
"warning",
);
return { block: true, reason }; return { block: true, reason };
} }
+98 -46
View File
@@ -23,11 +23,7 @@ import {
DEFAULT_MAX_LINES, DEFAULT_MAX_LINES,
} from "@mariozechner/pi-coding-agent"; } from "@mariozechner/pi-coding-agent";
import { BorderedLoader } from "@mariozechner/pi-coding-agent"; import { BorderedLoader } from "@mariozechner/pi-coding-agent";
import { import { matchesKey, Key, truncateToWidth } from "@mariozechner/pi-tui";
matchesKey,
Key,
truncateToWidth,
} from "@mariozechner/pi-tui";
import { Type } from "typebox"; import { Type } from "typebox";
// ── Types ─────────────────────────────────────────────────────────────────── // ── Types ───────────────────────────────────────────────────────────────────
@@ -275,7 +271,10 @@ async function fetchErrors(
token: string, token: string,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<ErrorListResponse> { ): Promise<ErrorListResponse> {
return fetchApi(url, token, (data) => { return fetchApi(
url,
token,
(data) => {
const obj = data as Record<string, unknown>; const obj = data as Record<string, unknown>;
if (!obj.errors || !Array.isArray(obj.errors)) { if (!obj.errors || !Array.isArray(obj.errors)) {
throw new Error( throw new Error(
@@ -285,7 +284,9 @@ async function fetchErrors(
); );
} }
return data as ErrorListResponse; return data as ErrorListResponse;
}, signal); },
signal,
);
} }
async function fetchError( async function fetchError(
@@ -293,7 +294,10 @@ async function fetchError(
token: string, token: string,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<ErrorDetailResponse> { ): Promise<ErrorDetailResponse> {
return fetchApi(url, token, (data) => { return fetchApi(
url,
token,
(data) => {
const obj = data as Record<string, unknown>; const obj = data as Record<string, unknown>;
if (!obj.error || typeof obj.error !== "object") { if (!obj.error || typeof obj.error !== "object") {
throw new Error( throw new Error(
@@ -303,7 +307,9 @@ async function fetchError(
); );
} }
return data as ErrorDetailResponse; return data as ErrorDetailResponse;
}, signal); },
signal,
);
} }
// ── TUI Theme ─────────────────────────────────────────────────────────────── // ── TUI Theme ───────────────────────────────────────────────────────────────
@@ -378,7 +384,10 @@ class ErrorBrowser {
private readonly baseUrl: string; private readonly baseUrl: string;
private readonly token: string; private readonly token: string;
private readonly requestRender: () => void; private readonly requestRender: () => void;
private readonly notify: (msg: string, level: "error" | "info" | "warning") => void; private readonly notify: (
msg: string,
level: "error" | "info" | "warning",
) => void;
constructor( constructor(
errors: ErrorListItem[], errors: ErrorListItem[],
@@ -411,7 +420,10 @@ class ErrorBrowser {
if (this.errors.length === 0) { if (this.errors.length === 0) {
this.cursorIndex = -1; this.cursorIndex = -1;
} else { } else {
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.errors.length - 1)); this.cursorIndex = Math.max(
0,
Math.min(this.cursorIndex, this.errors.length - 1),
);
} }
} }
@@ -423,7 +435,10 @@ class ErrorBrowser {
} else if (this.cursorIndex >= this.scrollOffset + maxEntries) { } else if (this.cursorIndex >= this.scrollOffset + maxEntries) {
this.scrollOffset = this.cursorIndex - maxEntries + 1; this.scrollOffset = this.cursorIndex - maxEntries + 1;
} }
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.errors.length - 1)); this.scrollOffset = Math.max(
0,
Math.min(this.scrollOffset, this.errors.length - 1),
);
} }
// ── HTTP helpers ─────────────────────────────────────────────────────── // ── HTTP helpers ───────────────────────────────────────────────────────
@@ -471,7 +486,11 @@ class ErrorBrowser {
this.invalidate(); this.invalidate();
this.requestRender(); this.requestRender();
const url = buildUrl(this.baseUrl, "/api/v1/errors", this.buildQueryParams()); const url = buildUrl(
this.baseUrl,
"/api/v1/errors",
this.buildQueryParams(),
);
const controller = new AbortController(); const controller = new AbortController();
this.currentAbortController = controller; this.currentAbortController = controller;
@@ -508,7 +527,11 @@ class ErrorBrowser {
this.invalidate(); this.invalidate();
this.requestRender(); this.requestRender();
const url = buildUrl(this.baseUrl, "/api/v1/errors", this.buildQueryParams()); const url = buildUrl(
this.baseUrl,
"/api/v1/errors",
this.buildQueryParams(),
);
const controller = new AbortController(); const controller = new AbortController();
this.currentAbortController = controller; this.currentAbortController = controller;
@@ -638,7 +661,10 @@ class ErrorBrowser {
const pageSize = Math.floor(this.visibleHeight / this.errorEntryLines); const pageSize = Math.floor(this.visibleHeight / this.errorEntryLines);
this.cursorIndex -= pageSize; this.cursorIndex -= pageSize;
moved = true; moved = true;
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("f"))) { } else if (
matchesKey(data, Key.pageDown) ||
matchesKey(data, Key.ctrl("f"))
) {
const pageSize = Math.floor(this.visibleHeight / this.errorEntryLines); const pageSize = Math.floor(this.visibleHeight / this.errorEntryLines);
this.cursorIndex += pageSize; this.cursorIndex += pageSize;
moved = true; moved = true;
@@ -689,7 +715,10 @@ class ErrorBrowser {
} else if (matchesKey(data, Key.pageUp)) { } else if (matchesKey(data, Key.pageUp)) {
this.detailScrollOffset -= this.visibleHeight; this.detailScrollOffset -= this.visibleHeight;
moved = true; moved = true;
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("f"))) { } else if (
matchesKey(data, Key.pageDown) ||
matchesKey(data, Key.ctrl("f"))
) {
this.detailScrollOffset += this.visibleHeight; this.detailScrollOffset += this.visibleHeight;
moved = true; moved = true;
} else if (matchesKey(data, Key.ctrl("b"))) { } else if (matchesKey(data, Key.ctrl("b"))) {
@@ -699,13 +728,19 @@ class ErrorBrowser {
this.detailScrollOffset = 0; this.detailScrollOffset = 0;
moved = true; moved = true;
} else if (matchesKey(data, Key.end) || matchesKey(data, "G")) { } else if (matchesKey(data, Key.end) || matchesKey(data, "G")) {
this.detailScrollOffset = Math.max(0, this.buildDetailLines().length - 1); this.detailScrollOffset = Math.max(
0,
this.buildDetailLines().length - 1,
);
moved = true; moved = true;
} }
if (moved) { if (moved) {
const maxOffset = Math.max(0, this.buildDetailLines().length - 1); const maxOffset = Math.max(0, this.buildDetailLines().length - 1);
this.detailScrollOffset = Math.max(0, Math.min(this.detailScrollOffset, maxOffset)); this.detailScrollOffset = Math.max(
0,
Math.min(this.detailScrollOffset, maxOffset),
);
this.invalidate(); this.invalidate();
} }
@@ -746,7 +781,9 @@ class ErrorBrowser {
for (let i = 1; i < viewportRows - 1; i++) { for (let i = 1; i < viewportRows - 1; i++) {
if (i === mid) { if (i === mid) {
result.push(truncateToWidth(theme.fg("muted", " Loading…"), width, "")); result.push(
truncateToWidth(theme.fg("muted", " Loading…"), width, ""),
);
} else { } else {
result.push(""); result.push("");
} }
@@ -809,12 +846,24 @@ class ErrorBrowser {
else if (error.status === "resolved") statusColor = "success"; else if (error.status === "resolved") statusColor = "success";
const mutedLabel = error.muted ? " [MUTED]" : ""; const mutedLabel = error.muted ? " [MUTED]" : "";
const statusBadge = theme.fg(statusColor, `[${error.status.toUpperCase()}]`); const statusBadge = theme.fg(
statusColor,
`[${error.status.toUpperCase()}]`,
);
// Line 1: cursor + status badge + kind + truncated reason // Line 1: cursor + status badge + kind + truncated reason
const reasonText = truncateReason(error.reason, Math.max(30, width - 25)); const reasonText = truncateReason(
error.reason,
Math.max(30, width - 25),
);
const line1 = `${cursor} ${statusBadge}${mutedLabel} ${error.kind}: ${reasonText}`; const line1 = `${cursor} ${statusBadge}${mutedLabel} ${error.kind}: ${reasonText}`;
result.push(truncateToWidth(isCursor ? theme.fg("accent", line1) : line1, width, "")); result.push(
truncateToWidth(
isCursor ? theme.fg("accent", line1) : line1,
width,
"",
),
);
// Line 2: source info // Line 2: source info
const sourceInfo = error.source_function const sourceInfo = error.source_function
@@ -857,9 +906,7 @@ class ErrorBrowser {
truncateToWidth( truncateToWidth(
theme.fg( theme.fg(
"dim", "dim",
hasMore hasMore ? ` l load more q quit` : ` — end of results — q quit`,
? ` l load more q quit`
: ` — end of results — q quit`,
), ),
width, width,
"", "",
@@ -972,13 +1019,13 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
parameters: Type.Object({ parameters: Type.Object({
status: Type.Optional( status: Type.Optional(
Type.Union([Type.Literal("resolved"), Type.Literal("unresolved")], { Type.Union([Type.Literal("resolved"), Type.Literal("unresolved")], {
description: description: "Filter by error status: 'resolved' or 'unresolved'.",
"Filter by error status: 'resolved' or 'unresolved'.",
}), }),
), ),
muted: Type.Optional( muted: Type.Optional(
Type.Boolean({ Type.Boolean({
description: "Filter by muted state. true = only muted, false = only unmuted.", description:
"Filter by muted state. true = only muted, false = only unmuted.",
}), }),
), ),
search: Type.Optional( search: Type.Optional(
@@ -1000,13 +1047,7 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
}), }),
), ),
}), }),
async execute( async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
_toolCallId,
params,
signal,
_onUpdate,
_ctx,
) {
// Early abort check // Early abort check
if (signal?.aborted) { if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] }; return { content: [{ type: "text", text: "Cancelled" }] };
@@ -1043,7 +1084,11 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
if (params.muted !== undefined && params.muted !== null) { if (params.muted !== undefined && params.muted !== null) {
queryParams.muted = String(params.muted); queryParams.muted = String(params.muted);
} }
if (params.search !== undefined && params.search !== null && params.search !== "") { if (
params.search !== undefined &&
params.search !== null &&
params.search !== ""
) {
queryParams.search = params.search; queryParams.search = params.search;
} }
if (params.limit !== undefined && params.limit !== null) { if (params.limit !== undefined && params.limit !== null) {
@@ -1075,8 +1120,18 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
// Handle empty results // Handle empty results
if (data.errors.length === 0) { if (data.errors.length === 0) {
return { return {
content: [{ type: "text", text: "No errors found matching the given filters." }], content: [
details: { total: data.total, count: 0, offset: data.offset, limit: data.limit }, {
type: "text",
text: "No errors found matching the given filters.",
},
],
details: {
total: data.total,
count: 0,
offset: data.offset,
limit: data.limit,
},
}; };
} }
@@ -1126,13 +1181,7 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
"The error ID (integer) to fetch full details for. Get this from fetch_production_errors.", "The error ID (integer) to fetch full details for. Get this from fetch_production_errors.",
}), }),
}), }),
async execute( async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
_toolCallId,
params,
signal,
_onUpdate,
_ctx,
) {
// Early abort check // Early abort check
if (signal?.aborted) { if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] }; return { content: [{ type: "text", text: "Cancelled" }] };
@@ -1256,7 +1305,10 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
); );
if (errorsData === null) { if (errorsData === null) {
ctx.ui.notify("Cancelled or fetch failed — check logs for details", "info"); ctx.ui.notify(
"Cancelled or fetch failed — check logs for details",
"info",
);
return; return;
} }
+50 -33
View File
@@ -21,11 +21,7 @@ import {
DEFAULT_MAX_LINES, DEFAULT_MAX_LINES,
} from "@mariozechner/pi-coding-agent"; } from "@mariozechner/pi-coding-agent";
import { Type } from "typebox"; import { Type } from "typebox";
import { import { matchesKey, Key, truncateToWidth } from "@mariozechner/pi-tui";
matchesKey,
Key,
truncateToWidth,
} from "@mariozechner/pi-tui";
// ── Types ─────────────────────────────────────────────────────────────────── // ── Types ───────────────────────────────────────────────────────────────────
@@ -80,7 +76,10 @@ class LogViewer {
this.visualMode = false; this.visualMode = false;
this.visualAnchor = 0; this.visualAnchor = 0;
this.scrollOffset = 0; this.scrollOffset = 0;
this.visibleHeight = Math.min(lines.length, Math.max(10, process.stdout.rows - chromeHeight)); this.visibleHeight = Math.min(
lines.length,
Math.max(10, process.stdout.rows - chromeHeight),
);
this.clampCursor(); this.clampCursor();
this.clampViewport(); this.clampViewport();
this.invalidate(); this.invalidate();
@@ -101,7 +100,10 @@ class LogViewer {
if (this.lines.length === 0) { if (this.lines.length === 0) {
this.cursorIndex = -1; this.cursorIndex = -1;
} else { } else {
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.lines.length - 1)); this.cursorIndex = Math.max(
0,
Math.min(this.cursorIndex, this.lines.length - 1),
);
} }
} }
@@ -129,7 +131,10 @@ class LogViewer {
} }
// Escape or q in normal mode closes the viewer // Escape or q in normal mode closes the viewer
if (!this.visualMode && (matchesKey(data, Key.escape) || matchesKey(data, "q"))) { if (
!this.visualMode &&
(matchesKey(data, Key.escape) || matchesKey(data, "q"))
) {
this.onClose?.(); this.onClose?.();
return; return;
} }
@@ -156,7 +161,10 @@ class LogViewer {
const start = Math.min(this.visualAnchor, this.cursorIndex); const start = Math.min(this.visualAnchor, this.cursorIndex);
const end = Math.max(this.visualAnchor, this.cursorIndex); const end = Math.max(this.visualAnchor, this.cursorIndex);
// this.lines[0] = newest, so reverse the slice for oldest-first // this.lines[0] = newest, so reverse the slice for oldest-first
const text = this.lines.slice(start, end + 1).reverse().join("\n"); const text = this.lines
.slice(start, end + 1)
.reverse()
.join("\n");
this.onCopy?.(text); this.onCopy?.(text);
} }
return; return;
@@ -184,7 +192,10 @@ class LogViewer {
} else if (matchesKey(data, Key.pageUp)) { } else if (matchesKey(data, Key.pageUp)) {
this.cursorIndex -= this.visibleHeight; this.cursorIndex -= this.visibleHeight;
moved = true; moved = true;
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("f"))) { } else if (
matchesKey(data, Key.pageDown) ||
matchesKey(data, Key.ctrl("f"))
) {
this.cursorIndex += this.visibleHeight; this.cursorIndex += this.visibleHeight;
moved = true; moved = true;
} else if (matchesKey(data, Key.ctrl("b"))) { } else if (matchesKey(data, Key.ctrl("b"))) {
@@ -237,9 +248,8 @@ class LogViewer {
const basePrefix = ` ${numStr}`; const basePrefix = ` ${numStr}`;
const isCursor = absIdx === this.cursorIndex; const isCursor = absIdx === this.cursorIndex;
const isSelected = this.visualMode const isSelected =
&& absIdx >= selStart this.visualMode && absIdx >= selStart && absIdx <= selEnd;
&& absIdx <= selEnd;
if (isCursor) { if (isCursor) {
// Cursor takes priority over selection highlight // Cursor takes priority over selection highlight
@@ -349,13 +359,7 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
}), }),
), ),
}), }),
async execute( async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
_toolCallId,
params,
signal,
_onUpdate,
_ctx,
) {
// Early abort check // Early abort check
if (signal?.aborted) { if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] }; return { content: [{ type: "text", text: "Cancelled" }] };
@@ -498,7 +502,10 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
); );
if (logLines === null) { if (logLines === null) {
ctx.ui.notify("Cancelled or fetch failed — check logs for details", "info"); ctx.ui.notify(
"Cancelled or fetch failed — check logs for details",
"info",
);
return; return;
} }
@@ -519,7 +526,10 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
let loading = false; let loading = false;
const visibleLogLines = () => const visibleLogLines = () =>
Math.min(logLinesMut.length, Math.max(10, process.stdout.rows - chromeHeight)); Math.min(
logLinesMut.length,
Math.max(10, process.stdout.rows - chromeHeight),
);
const viewer = new LogViewer(logLinesMut, visibleLogLines()); const viewer = new LogViewer(logLinesMut, visibleLogLines());
@@ -553,7 +563,8 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
return { return {
render(width: number) { render(width: number) {
const pct = viewer.total > 0 const pct =
viewer.total > 0
? Math.round((viewer.offset / viewer.total) * 100) ? Math.round((viewer.offset / viewer.total) * 100)
: 0; : 0;
const from = viewer.offset + 1; const from = viewer.offset + 1;
@@ -565,20 +576,28 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
const result: string[] = []; const result: string[] = [];
const border = theme.fg("accent", "─".repeat(width)); const border = theme.fg("accent", "─".repeat(width));
result.push(border); result.push(border);
result.push(truncateToWidth( result.push(
truncateToWidth(
theme.fg("accent", theme.bold(" Production Logs")), theme.fg("accent", theme.bold(" Production Logs")),
width, width,
"", "",
)); ),
);
const countText = loading const countText = loading
? " Refreshing..." ? " Refreshing..."
: ` ${logLinesMut.length} lines`; : ` ${logLinesMut.length} lines`;
result.push(truncateToWidth(theme.fg("muted", countText), width, "")); result.push(
result.push(truncateToWidth(theme.fg("dim", "─".repeat(40)), width, "")); truncateToWidth(theme.fg("muted", countText), width, ""),
);
result.push(
truncateToWidth(theme.fg("dim", "─".repeat(40)), width, ""),
);
const infoText = ` Lines ${from}-${to} of ${logLinesMut.length} (${pct}%)`; const infoText = ` Lines ${from}-${to} of ${logLinesMut.length} (${pct}%)`;
result.push(truncateToWidth(theme.fg("dim", infoText), width, "")); result.push(
truncateToWidth(theme.fg("dim", infoText), width, ""),
);
// Log lines // Log lines
const logRendered = viewer.render(width, theme); const logRendered = viewer.render(width, theme);
@@ -590,11 +609,9 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
: viewer.visualMode : viewer.visualMode
? " VISUAL: j/k extend · y copy · Esc cancel · Enter copy line" ? " VISUAL: j/k extend · y copy · Esc cancel · Enter copy line"
: " ↑↓/jk scroll · PgUp/PgDn page · Home/End jump · v visual · Enter copy line · r refresh · Esc close"; : " ↑↓/jk scroll · PgUp/PgDn page · Home/End jump · v visual · Enter copy line · r refresh · Esc close";
result.push(truncateToWidth( result.push(
theme.fg("dim", helpLine), truncateToWidth(theme.fg("dim", helpLine), width, ""),
width, );
"",
));
result.push(border); result.push(border);
return result; return result;
+22 -11
View File
@@ -33,7 +33,10 @@ const S3_REGION = "nbg1";
function humanSize(bytes: number): string { function humanSize(bytes: number): string {
if (bytes === 0) return "0 B"; if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"]; const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1,
);
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`; return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
} }
@@ -152,7 +155,10 @@ export default function s3BrowserExtension(pi: ExtensionAPI) {
); );
if (objects === null) { if (objects === null) {
ctx.ui.notify("Cancelled or fetch failed — check logs for details", "info"); ctx.ui.notify(
"Cancelled or fetch failed — check logs for details",
"info",
);
return; return;
} }
@@ -174,7 +180,8 @@ export default function s3BrowserExtension(pi: ExtensionAPI) {
// Summary header line // Summary header line
const summary = `${objects.length} files · ${humanSize(totalSize)} total`; const summary = `${objects.length} files · ${humanSize(totalSize)} total`;
const selectedKey = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => { const selectedKey = await ctx.ui.custom<string | null>(
(tui, theme, _kb, done) => {
const container = new Container(); const container = new Container();
// Top border // Top border
@@ -184,16 +191,19 @@ export default function s3BrowserExtension(pi: ExtensionAPI) {
// Header // Header
container.addChild( container.addChild(
new Text(theme.fg("accent", theme.bold("S3 Backups — ffmusiclibrary/prod/")), 1, 0), new Text(
); theme.fg(
container.addChild( "accent",
new Text(theme.fg("muted", summary), 1, 0), theme.bold("S3 Backups — ffmusiclibrary/prod/"),
),
1,
0,
),
); );
container.addChild(new Text(theme.fg("muted", summary), 1, 0));
// Spacer before list // Spacer before list
container.addChild( container.addChild(new Text(theme.fg("dim", "─".repeat(40)), 1, 0));
new Text(theme.fg("dim", "─".repeat(40)), 1, 0),
);
// SelectList // SelectList
const selectList = new SelectList(items, Math.min(items.length, 20), { const selectList = new SelectList(items, Math.min(items.length, 20), {
@@ -233,7 +243,8 @@ export default function s3BrowserExtension(pi: ExtensionAPI) {
tui.requestRender(); tui.requestRender();
}, },
}; };
}); },
);
if (selectedKey) { if (selectedKey) {
const obj = objects.find((o) => o.key === selectedKey); const obj = objects.find((o) => o.key === selectedKey);
+10 -9
View File
@@ -1,6 +1,7 @@
--- ---
description: Create a new backlog task description: Create a new backlog task
--- ---
Let's create a new backlog task via a predefined process. **IT'S CRITICAL YOU FOLLOW THIS PROCESS**. Let's create a new backlog task via a predefined process. **IT'S CRITICAL YOU FOLLOW THIS PROCESS**.
To start, ask me for: To start, ask me for:
@@ -21,30 +22,30 @@ For a plan to be valid, it has to cover these requirements:
1. **Objective alignment** — The plan clearly states how it achieves the stated objective of the issue, with a direct mapping between the problem and the proposed solution. 1. **Objective alignment** — The plan clearly states how it achieves the stated objective of the issue, with a direct mapping between the problem and the proposed solution.
2. **Simplicity and alternatives considered** — The plan identifies the simplest viable approach. It documents alternatives that were evaluated, explains why they were rejected or deferred, and 2. **Simplicity and alternatives considered** — The plan identifies the simplest viable approach. It documents alternatives that were evaluated, explains why they were rejected or deferred, and
justifies why the chosen approach is the right trade-off for the objective. justifies why the chosen approach is the right trade-off for the objective.
3. **Completeness and sequencing** — The plan covers every implementation step in a logical order. No gaps exist between "current state" and "done state." Dependencies between steps are 3. **Completeness and sequencing** — The plan covers every implementation step in a logical order. No gaps exist between "current state" and "done state." Dependencies between steps are
explicit so the plan can be executed sequentially without backtracking. explicit so the plan can be executed sequentially without backtracking.
4. **Verifiability** — Each implementation step includes concrete verification instructions (tests to run, manual checks to perform, queries to validate) that prove the step was completed 4. **Verifiability** — Each implementation step includes concrete verification instructions (tests to run, manual checks to perform, queries to validate) that prove the step was completed
correctly before moving on. correctly before moving on.
5. **Architecture impact analysis** — The plan identifies all architectural touchpoints affected by the change (schemas, contexts, PubSub topics, supervision tree, routes, external APIs, UI 5. **Architecture impact analysis** — The plan identifies all architectural touchpoints affected by the change (schemas, contexts, PubSub topics, supervision tree, routes, external APIs, UI
components) and describes how each is impacted, including any migration or deprecation path. components) and describes how each is impacted, including any migration or deprecation path.
6. **Performance profile** — The plan explains the performance characteristics of the chosen approach: expected runtime complexity, database query patterns (including N+1 risks), memory 6. **Performance profile** — The plan explains the performance characteristics of the chosen approach: expected runtime complexity, database query patterns (including N+1 risks), memory
footprint, and any latency or throughput implications under realistic load. footprint, and any latency or throughput implications under realistic load.
7. **Benchmarking requirements** — The plan identifies whether one-off or ongoing benchmarks are needed to validate or monitor the performance profile, and if so, specifies what to measure, how 7. **Benchmarking requirements** — The plan identifies whether one-off or ongoing benchmarks are needed to validate or monitor the performance profile, and if so, specifies what to measure, how
to measure it, and what thresholds define acceptable performance. to measure it, and what thresholds define acceptable performance.
8. **Cost profile** — If the implementation consumes paid resources (API calls, compute, storage, third-party services), the plan includes a cost estimate or model so the financial impact is 8. **Cost profile** — If the implementation consumes paid resources (API calls, compute, storage, third-party services), the plan includes a cost estimate or model so the financial impact is
understood before implementation begins. understood before implementation begins.
9. **Production infrastructure steps** — Any manual changes required in production (environment variables, service provisioning, database migrations with special handling, DNS changes, firewall 9. **Production infrastructure steps** — Any manual changes required in production (environment variables, service provisioning, database migrations with special handling, DNS changes, firewall
rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions. rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions.
10. **Documentation updates** — The plan enumerates which project documentation files must be created or updated as part of the implementation (e.g., `docs/architecture.md`, 10. **Documentation updates** — The plan enumerates which project documentation files must be created or updated as part of the implementation (e.g., `docs/architecture.md`,
`docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs. `docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs.
At the end, do not offer to start implementation. Instead, offer to start a new session so that I can review the plan via `/review-task-plan`. At the end, do not offer to start implementation. Instead, offer to start a new session so that I can review the plan via `/review-task-plan`.
+9 -9
View File
@@ -10,30 +10,30 @@ For a plan to be valid, it has to cover these requirements:
1. **Objective alignment** — The plan clearly states how it achieves the stated objective of the issue, with a direct mapping between the problem and the proposed solution. 1. **Objective alignment** — The plan clearly states how it achieves the stated objective of the issue, with a direct mapping between the problem and the proposed solution.
2. **Simplicity and alternatives considered** — The plan identifies the simplest viable approach. It documents alternatives that were evaluated, explains why they were rejected or deferred, and 2. **Simplicity and alternatives considered** — The plan identifies the simplest viable approach. It documents alternatives that were evaluated, explains why they were rejected or deferred, and
justifies why the chosen approach is the right trade-off for the objective. justifies why the chosen approach is the right trade-off for the objective.
3. **Completeness and sequencing** — The plan covers every implementation step in a logical order. No gaps exist between "current state" and "done state." Dependencies between steps are 3. **Completeness and sequencing** — The plan covers every implementation step in a logical order. No gaps exist between "current state" and "done state." Dependencies between steps are
explicit so the plan can be executed sequentially without backtracking. explicit so the plan can be executed sequentially without backtracking.
4. **Verifiability** — Each implementation step includes concrete verification instructions (tests to run, manual checks to perform, queries to validate) that prove the step was completed 4. **Verifiability** — Each implementation step includes concrete verification instructions (tests to run, manual checks to perform, queries to validate) that prove the step was completed
correctly before moving on. correctly before moving on.
5. **Architecture impact analysis** — The plan identifies all architectural touchpoints affected by the change (schemas, contexts, PubSub topics, supervision tree, routes, external APIs, UI 5. **Architecture impact analysis** — The plan identifies all architectural touchpoints affected by the change (schemas, contexts, PubSub topics, supervision tree, routes, external APIs, UI
components) and describes how each is impacted, including any migration or deprecation path. components) and describes how each is impacted, including any migration or deprecation path.
6. **Performance profile** — The plan explains the performance characteristics of the chosen approach: expected runtime complexity, database query patterns (including N+1 risks), memory 6. **Performance profile** — The plan explains the performance characteristics of the chosen approach: expected runtime complexity, database query patterns (including N+1 risks), memory
footprint, and any latency or throughput implications under realistic load. footprint, and any latency or throughput implications under realistic load.
7. **Benchmarking requirements** — The plan identifies whether one-off or ongoing benchmarks are needed to validate or monitor the performance profile, and if so, specifies what to measure, how 7. **Benchmarking requirements** — The plan identifies whether one-off or ongoing benchmarks are needed to validate or monitor the performance profile, and if so, specifies what to measure, how
to measure it, and what thresholds define acceptable performance. to measure it, and what thresholds define acceptable performance.
8. **Cost profile** — If the implementation consumes paid resources (API calls, compute, storage, third-party services), the plan includes a cost estimate or model so the financial impact is 8. **Cost profile** — If the implementation consumes paid resources (API calls, compute, storage, third-party services), the plan includes a cost estimate or model so the financial impact is
understood before implementation begins. understood before implementation begins.
9. **Production infrastructure steps** — Any manual changes required in production (environment variables, service provisioning, database migrations with special handling, DNS changes, firewall 9. **Production infrastructure steps** — Any manual changes required in production (environment variables, service provisioning, database migrations with special handling, DNS changes, firewall
rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions. rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions.
10. **Documentation updates** — The plan enumerates which project documentation files must be created or updated as part of the implementation (e.g., `docs/architecture.md`, 10. **Documentation updates** — The plan enumerates which project documentation files must be created or updated as part of the implementation (e.g., `docs/architecture.md`,
`docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs. `docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs.
At the end, do not offer to start implementation. Instead, offer to start a new session so that I can review the plan via `/review-task-plan`. At the end, do not offer to start implementation. Instead, offer to start a new session so that I can review the plan via `/review-task-plan`.
+1 -5
View File
@@ -13,9 +13,5 @@
".gnupg/", ".gnupg/",
"~/.pi/agent/sessions/" "~/.pi/agent/sessions/"
], ],
"blocked_commands": [ "blocked_commands": ["printenv", "\\benv\\b", "\\bset\\b"]
"printenv",
"\\benv\\b",
"\\bset\\b"
]
} }
+2 -6
View File
@@ -1,8 +1,4 @@
{ {
"skills": [ "skills": ["../.claude/skills"],
"../.claude/skills" "packages": ["npm:pi-mcp-adapter"]
],
"packages": [
"npm:pi-mcp-adapter"
]
} }