22 KiB
id, title, status, assignee, created_date, updated_date, labels, dependencies, modified_files, parent_task_id, priority, ordinal
| id | title | status | assignee | created_date | updated_date | labels | dependencies | modified_files | parent_task_id | priority | ordinal | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ML-164 | Create pi extension for interactive error browsing | Done | 2026-05-04 08:08 | 2026-05-04 12:44 |
|
|
ML-161 | medium | 6000 |
Description
Build a pi extension that provides an interactive TUI for browsing production errors, using the fetch_production_errors and fetch_production_error tools from the parent task.
This extension gives the user (and LLM) a browseable interface for production errors, accessible via a slash command like /prod-errors.
Extension features
-
/prod-errorscommand — Opens an interactive TUI usingctx.ui.custom():- Lists recent errors (unresolved first, then by last_occurrence_at desc)
- Shows key metadata: reason (truncated), kind, source location, occurrence count, last seen, status badge, muted indicator
- Keyboard navigation: up/down to select, Enter to view details, Escape to close
- Filter toggle: show/hide resolved, show/hide muted (keyboard shortcuts)
- Pagination: load more errors as user scrolls
-
Error detail view (Enter on an error):
- Full reason text
- Source location with link-like formatting
- Status, muted, fingerprint
- Timeline of occurrences with timestamps
- Stacktrace display (collapsible per occurrence)
- Context display (request path, LiveView, etc.)
- Breadcrumbs if present
-
UI patterns (consistent with existing
/prod-logsextension):- Uses
pi.exec("curl", ...)to call the API - Reads credentials from
resolveVar()(same pattern as prod-logs) - Keyboard shortcuts displayed as hints
- Theme-aware rendering via
ctx.ui.theme
- Uses
TUI component design
╔══════════════════════════════════════════════════╗
║ Production Errors [12 errors] ║
╠══════════════════════════════════════════════════╣
║ ▶ [UNRESOLVED] FunctionClauseError ║
║ MusicLibrary.Foo.bar/2 lib/foo.ex:42 ║
║ 23 occurrences · last seen 2h ago ║
║ ──────────────────────────────────────────────── ║
║ [RESOLVED] MatchError ║
║ MusicLibrary.Baz.qux/1 lib/baz.ex:15 ║
║ 5 occurrences · last seen 3d ago ║
║ ──────────────────────────────────────────────── ║
║ [MUTED] KeyError (key :foo not found) ║
║ MusicLibrary.Other.func/3 lib/other.ex:99 ║
║ 1 occurrence · last seen 7d ago ║
║ ──────────────────────────────────────────────── ║
║ ║
║ ↑↓ navigate ↵ details r toggle resolved ║
║ m toggle muted q quit ║
╚══════════════════════════════════════════════════╝
File location
.pi/extensions/prod-errors/index.ts (new extension, separate from prod-logs)
The prod-logs extension already provides the resolveVar pattern and fetchLogs function. This extension follows the same conventions but for error_tracker data.
Implementation Plan
Implementation Plan
Objective alignment
Build a pi extension (.pi/extensions/prod-errors/index.ts) that provides an interactive TUI for browsing production errors, accessible via /prod-errors. The extension calls the ML-162 JSON API endpoints (GET /api/v1/errors for list, GET /api/v1/errors/:id for detail) using the ML-163 auth pattern (PI_API_TOKEN + PI_SERVICE_FQDN_WEB). The TUI follows the same interactive-component conventions as the existing prod-logs extension.
Dependency chain: ML-161 → ML-162 (API endpoint) → ML-163 (pi tools for LLM) → ML-164 (interactive TUI). ML-162 must be deployed first. ML-163 is NOT a hard dependency — the TUI calls the API directly (not through pi tools), giving it access to raw JSON with nested occurrences.
Data shape contract with ML-162
Key fields used by the list view: id, kind, reason, source_line, source_function, status, muted, last_occurrence_at, occurrence_count (see note). occurrence_count gap: ML-162's current plan omits this from the list endpoint. Recommendation: ML-162 should include it via LEFT JOIN + GROUP BY. If not, the TUI gracefully omits the count line and shows only the relative timestamp.
Alternatives considered
- Build TUI using pi tools — Rejected. Tools format/truncate text for LLM; the TUI needs structured JSON for interactive rendering.
- Merge into prod-logs extension — Rejected. Already 550 lines; mixing unrelated concerns bloats the file.
- Use
pi.exec("curl")instead offetch()— Rejected.fetch()provides abort support and JSON parsing. The prod-logs extension usesfetch(); the task description's mention of curl is outdated. - Collapsible stacktraces — Deferred to follow-up. Adds complexity; initial version shows all stacktraces inline.
- Auto-load on scroll-to-bottom — Deferred. No scroll events in
handleInput(); explicitlkey is simpler and gives user control. - Relative time display — Included. Simple arithmetic: "just now" / "Xm ago" / "Xh ago" / "Xd ago".
Architecture impact analysis
| Touchpoint | Impact |
|---|---|
.pi/extensions/prod-errors/index.ts |
New file — ~400 lines: ErrorBrowser class, helpers, command registration |
.pi/extensions/prod-errors/package.json |
New file — minimal { name, private, description } |
.pi/extensions/prod-logs/index.ts |
No change |
| All Elixir modules, router, PubSub, supervision tree | No change — purely a pi extension |
| Pi env vars | Reuses PI_API_TOKEN and PI_SERVICE_FQDN_WEB from ML-163 |
| Existing pi extensions | No change — /prod-errors is a new command |
Performance profile
- List fetch: One
GET /api/v1/errorsper page/filter change. ~5-20KB response, ~50-150 rendered lines. - Detail fetch: One
GET /api/v1/errors/:idper Enter. Up to ~500KB for noisy errors with many occurrences. - N+1 risk: None. Only one detail fetch at a time per Enter press.
- Memory: JSON + rendered cache bounded by single-page or single-detail data. Caches invalidated on width/state change.
- Rendering: O(lines) per frame. Render caching avoids recomputation on unchanged state.
- Abort support: In-flight requests aborted before new ones start (filter toggles, load more, detail fetch).
Benchmarking requirements
No dedicated benchmarks. Thin HTTP client with fixed-size rendering. Server-side API (ML-162) is the bottleneck. If detail view is slow for 100+ occurrences, future optimization: cap displayed occurrences at 50 with a note.
Cost profile
No paid resources. Makes HTTP requests to the project's own server. No third-party APIs, no additional compute/storage.
Implementation steps (sequential order)
Prerequisites: ML-162 complete; PI_API_TOKEN and PI_SERVICE_FQDN_WEB configured.
Step 1: Create .pi/extensions/prod-errors/package.json
Minimal package.json matching the prod-logs pattern:
{
"name": "prod-errors",
"private": true,
"description": "Interactive TUI for browsing production errors"
}
Verification: ls -la .pi/extensions/prod-errors/package.json — file must exist with valid JSON.
Step 2: Create the extension skeleton — types, helpers, HTTP functions
Create .pi/extensions/prod-errors/index.ts with:
-
Imports:
ExtensionAPIfrom@mariozechner/pi-coding-agent;matchesKey,Key,truncateToWidthfrom@mariozechner/pi-tui;BorderedLoaderfrom@mariozechner/pi-coding-agent. -
TypeScript interfaces for API responses (matching ML-162 JSON shapes):
ErrorListItem,ErrorsListResponse,StacktraceLine,Occurrence,ErrorDetail. Full interface definitions in the implementation notes. -
Themeinterface:{ fg(color: string, text: string): string; bold(text: string): string }— same as prod-logs. -
resolveVar(name: string): string | undefined— identical to prod-logs helper. -
formatRelativeTime(iso8601: string): string— seconds diff fromDate.now(), format as "just now" / "Xm ago" / "Xh ago" / "Xd ago". -
truncateReason(reason: string, maxLen: number): string— truncate to maxLen, append "...", take only first line. -
fetchErrors(params, signal?): Promise<ErrorsListResponse>—GET {base}/api/v1/errorswith query params (status,muted,search,limit,offset). Bearer auth. Throws on non-2xx or JSON parse failure. -
fetchErrorDetail(id, signal?): Promise<ErrorDetail>—GET {base}/api/v1/errors/:id. Same auth. Throws on non-2xx (including 404) or JSON parse failure.
Verification: Use /reload and the pi eval REPL to smoke-test helpers in isolation before wiring into the TUI:
formatRelativeTime(new Date(Date.now() - 3600000).toISOString())→ "1h ago"formatRelativeTime(new Date().toISOString())→ "just now"truncateReason("very long error message...", 30)→ truncated with "…"- Call
fetchErrors({ limit: 1 })against a running local Phoenix server with seeded data → verify JSON shape matchesErrorsListResponse - Call
fetchErrorDetail(1)→ verify JSON shape matchesErrorDetail
Step 3: Implement the ErrorBrowser class — list view
A class managing list view state and rendering, following the LogViewer pattern from prod-logs. State includes: mode ("list"/"detail"/"loading"), errors[], total, offset, limit (50), showResolved, showMuted, cursorIndex, scrollOffset, selectedError?, detailScrollOffset, loadingText. Callbacks: onClose, onFetchErrors, onFetchErrorDetail.
List-mode keyboard shortcuts: ↑/k ↓/j navigate cursor, PgUp/PgDn page, Home/g top, End/G bottom, Enter → detail fetch, r toggle resolved, m toggle muted, l load more, Escape/q close.
List rendering: Each error renders as 3 lines (cursor indicator ▶/ , status badge colored via theme, kind, truncated reason; {function} {source_line}; last seen {relative} with optional {count} occurrences · prefix). Cursor line: theme.fg("accent"). Unresolved badge: theme.fg("error"). Resolved badge: theme.fg("success"). Muted: theme.fg("muted"). Separators: theme.fg("dim"). Header/footer borders: theme.fg("accent"). Keyboard hints: theme.fg("dim").
Clamping/viewport: Same as LogViewer — clampCursor(), clampViewport(), visibleHeight computed from process.stdout.rows - chromeHeight (~8 lines for chrome).
Caching: Cache rendered lines when width and state unchanged. Invalidate on any state mutation.
Verification (requires ML-162 API running locally):
/reload→/prod-errors→ TUI opens with error list (or empty state).j/kmoves cursor.rtoggles resolved.mtoggles muted.lloads next page.Entertriggers detail fetch.q/Escapecloses TUI.- Missing
PI_API_TOKEN→ clear error notification.
Step 4: Implement the detail view
Extend ErrorBrowser with detail rendering. On Enter in list: set mode = "loading", fetch error detail, on success set mode = "detail", on failure notify and return to list.
Detail-mode keyboard shortcuts: Escape → back to list. ↑/k ↓/j scroll, PgUp/PgDn page, Home/g/End/G jump, Enter copies current line to editor.
Detail rendering: Build content array with sections (Reason, Status, Source, Fingerprint, First/Last occurrence, Total occurrences), then occurrences section with numbered entries each showing inserted_at, reason, context (key: value pairs), breadcrumbs (bullet list), stacktrace ({app} / {module}.{function}/{arity} {file}:{line}). Slice by detailScrollOffset. Omit sections when data is empty ({} context, [] breadcrumbs, zero occurrences → "No occurrences recorded").
Loading state: Inline "Loading…" overlay centered in viewport when mode === "loading".
Verification:
- Enter on error → detail shows all sections + occurrences with stacktraces.
j/kscrolls detail.PgUp/PgDnpages.Escapereturns to list with cursor preserved.- Enter on detail line → line copied to editor (prod-logs pattern).
- Error with no occurrences → "No occurrences recorded". Empty context/breadcrumbs → sections omitted.
Step 5: Register the /prod-errors command
Register via pi.registerCommand("prod-errors", ...). Handler flow:
- Validate
PI_SERVICE_FQDN_WEBandPI_API_TOKEN— notify and return if missing. - Show
BorderedLoaderwhile callingfetchErrors({ limit: 50, offset: 0 }). - On null/empty: notify and return.
- Show
ErrorBrowserviactx.ui.custom()— wire up callbacks, handle re-renders. - On close: notify "Error browser closed".
In-browser fetches (filter toggles, load more, detail) use an inline loading indicator, not nested BorderedLoader.
Verification:
/prod-errors→ loader → browser. Toggle filters → inline loading. Load more → inline loading. Detail → inline loading.- Bad
PI_SERVICE_FQDN_WEB→ useful error notification. All keyboard shortcuts work.
Step 6: Edge cases and error handling
| Scenario | Handling |
|---|---|
| Empty list (API returned zero errors) | "No production errors found" in TUI |
| All errors filtered out by current toggles | "No errors match the current filters" (only shown when the unfiltered list was non-empty; track with a totalUnfiltered field set on the initial fetch) |
| API non-2xx | Error notification with status code; keep TUI open |
| Unexpected JSON shape | Catch TypeError, "Unexpected API response format" notification |
| Network timeout/refused | Catch fetch error, show notification |
| Double Enter while loading | Ignore (mode === "loading" guard) |
| Rapid filter toggles | Abort in-flight request via AbortController before new fetch |
| Filter changes invalidate pages | Reset offset to 0, replace errors with first page |
| Load more at end | Show "— end of results —", ignore l |
| Zero occurrences | "No occurrences recorded" |
| Empty context/breadcrumbs | Omit those sections |
| Missing stacktrace fields | Show "—" |
| Narrow terminal (< 40 cols) | Render with heavy truncation (acceptable) |
Abort pattern: Store currentAbortController reference; call .abort() before each new fetch; catch AbortError and return null.
Verification:
- Rapid filter toggle: Press
r5 times rapidly → observe only the last fetch's results are displayed (previous requests were aborted). Requests aborted viaAbortControllershould not update state. - End-of-results: Navigate to the last page with
l, then presslagain → observe "— end of results —" divider and subsequentlpresses are ignored. - Empty context/breadcrumbs: View a detail for an error that has occurrences with
context: {}andbreadcrumbs: []→ observe those section headings are entirely omitted from the detail rendering. - Network error: Set
PI_SERVICE_FQDN_WEBtohttp://localhost:9999(unreachable port) and open/prod-errors→ observe a clear network error notification and the TUI does not crash. - Filters exclude everything: Start with a list that has errors, set both
showResolved=falseandshowMuted=false, then resolve and mute every error in the database via the ErrorTracker dashboard. Re-open/prod-errors→ observe "No errors match the current filters" (not "No production errors found"). - No errors at all: Truncate the error_tracker tables and open
/prod-errors→ observe "No production errors found".
Step 7: Integration verification (end-to-end)
Seed 15+ errors with varied statuses and 3+ occurrences each. Use the ML-162 error fixtures from test/support/fixtures/errors_fixtures.ex — insert error and occurrence records via MusicLibrary.Repo with ErrorTracker.Error and ErrorTracker.Occurrence structs (see ML-162's "Test data seeding strategy" for the exact insertion pattern, including the fingerprint hex-string encoding and Stacktrace struct construction). Ensure at least: 5 unresolved, 5 resolved, 2 muted, 2 with no occurrences, 1 with 10+ occurrences for scroll testing.
Full walkthrough: list rendering, status badges, muted indicator, relative timestamps, filter toggles, detail view with occurrences/stacktraces/context/breadcrumbs, copy-to-editor, Escape back to list, q close. Verify that after ML-162 is deployed and occurrence_count is available in the list endpoint, the count line appears in the list view. Run mix test for no Elixir regressions.
Production Changes
No new changes beyond ML-162/ML-163: PI_API_TOKEN and PI_SERVICE_FQDN_WEB are already required.
Documentation updates
docs/architecture.md— Addprod-errorspi extension entry (location, purpose, slash command, required env vars).docs/production-infrastructure.md— No changes needed if ML-163 docs are merged. Otherwise include env var entries.- No changes to
docs/project-conventions.mdordocs/available-tasks.md.
Dependencies
- ML-162: Required (API endpoints). Deploy first.
- ML-163: Not hard dependency (TUI calls API directly), but defines auth pattern.
@mariozechner/pi-coding-agentand@mariozechner/pi-tui: Already available.- No new npm dependencies.
Implementation Notes
Implementation Notes
What was built
Added the /prod-errors interactive TUI command to the existing .pi/extensions/prod-errors/ extension (which already contained the fetch_production_errors and fetch_production_error LLM tools from ML-163).
Files changed
.pi/extensions/prod-errors/index.ts— Added ErrorBrowser class (~540 lines), Theme interface, formatRelativeTime/truncateReason helpers, and/prod-errorscommand registration. Total file grew from ~360 to ~1396 lines..pi/extensions/prod-errors/package.json— Updated description to mention TUI command.docs/production-infrastructure.md— Added/prod-errorsto the pi coding agent tools table and added command description.
Key design decisions
- Reuses existing HTTP functions —
fetchErrors()andfetchError()from ML-163 are reused directly by the ErrorBrowser class. The class builds URLs via the existingbuildUrl()helper. - Flat rendering (not Container/Text) — Follows the prod-logs pattern of building arrays of styled strings, not the component-based approach using Container/Text/Box. This is simpler and directly compatible with the
ctx.ui.custom()API. - Self-contained class — ErrorBrowser manages all state, navigation, rendering, and async operations internally. It receives
requestRenderandnotifycallbacks from the command handler for I/O. - Occurrence count omission — The list endpoint doesn't include
occurrence_count(per ML-162 spec), so the list view shows relative timestamps instead. - No collapsible stacktraces — Deferred per implementation plan. Initial version shows all stacktraces inline with scrolling.
- Explicit load-more — No auto-load on scroll-to-bottom. Uses
lkey for user-controlled pagination.