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>
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.ts → notification-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/