singularity-forge/docs/DESIGN.md
Mikael Hugo 16ff608d80 feat: implement ADR-001 gitignore split and fill placeholder docs
Gitignore (core change):
- Remove stale blanket .sf/ entries from .gitignore (migrated to
  .git/info/exclude on 2026-04-29, never cleaned up)
- gitignore.ts: split SF_RUNTIME_EXCLUSION_PATTERNS into two modes —
  SF_SYMLINK_EXCLUSION_PATTERNS (blanket .sf for symlink repos where
  git cannot traverse the symlink) and SF_RUNTIME_EXCLUSION_PATTERNS
  (granular runtime-only patterns for directory repos, enabling
  .sf/milestones/ and other durable planning artifacts to be tracked)
- ensureGitInfoExclude() now detects symlink vs directory and writes
  the correct patterns, handling transitions between modes cleanly
- ADR-001 status: Proposed → Accepted

Docs:
- Fill 11 placeholder scaffold docs with real SF-specific content:
  PLANS, DESIGN, PRODUCT_SENSE, QUALITY_SCORE, RELIABILITY, SECURITY,
  design-docs/index.md, exec-plans/active, exec-plans/completed,
  exec-plans/tech-debt-tracker, records/index
- Add records note: docs/records/2026-05-01-repo-vcs-and-notifications.md
- ADR-008 status: Accepted → Proposed (deferred — not applicable to
  current usage model where Claude Code assists externally, not as a
  Pi provider inside SF's dispatch loop)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 22:32:28 +02:00

2.6 KiB

Design

SF's UI is a terminal application built on the Pi TUI framework (@mariozechner/pi-tui). These are the binding constraints any UI work must respect.

The Cardinal Rule: Line Width

Every line returned from render(width) must not exceed width in visible characters. Exceeding it causes terminal line-wrapping, cursor misposition, and visual corruption the framework cannot fix.

Use the Pi TUI utilities — never raw string.length:

import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";

visibleWidth("\x1b[32mHello\x1b[0m");          // 5, not 14
truncateToWidth("Very long text here", 10);    // "Very lo..."
wrapTextWithAnsi("\x1b[32mlong green\x1b[0m", 15); // preserves ANSI per line

visibleWidth strips ANSI escape codes before measuring. truncateToWidth preserves ANSI codes in the truncated output. Use these everywhere a line's display length matters.

Render Pattern

render(width: number): string[] {
  const lines: string[] = [];
  lines.push(truncateToWidth(`  ${prefix}${content}`, width));

  const labelWidth = visibleWidth(label);
  const available = width - labelWidth - 4; // padding
  lines.push(`  ${label}: ${truncateToWidth(value, available)}`);

  return lines;
}

Overlays and Modals

Floating panels use the Pi TUI overlay pattern: they render at a fixed position within the terminal bounds and must still respect the outer width constraint. An overlay that overflows its bounds causes the same wrapping corruption as any other component.

Use ctx.ui.dialog() for modal user input. Use ctx.ui.notify() for transient non-blocking notices. Persistent notification state goes through notification-store.tsnotification-overlay.ts.

Theming

Colors and styles come from the Pi TUI theme system, not from hardcoded ANSI codes. Access the active theme via the ExtensionContext. Respect theme changes: components must re-render when the theme changes (implement onThemeChange if caching rendered output).

IME and Focus

Interactive input components must implement the Focusable interface to receive keyboard events correctly, especially for IME (input method editor) support on non-ASCII keyboards. Components that handle key input but do not implement Focusable will silently swallow events.

Performance

Cache rendered output when the underlying data hasn't changed. Invalidate the cache on data change or theme change. Do not re-render on every tick. The TUI framework calls render() frequently; rendering must be cheap.

Reference

Full TUI documentation: docs/dev/pi-ui-tui/