diff --git a/README.md b/README.md index c553341..8d9d55d 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,16 @@ func main() { ### πŸ–₯️ Advanced Terminal UI -- **Syntax Highlighting**: Full syntax highlighting for diffs and code blocks -- **Diff Viewer**: Interactive unified diff viewer with color-coded changes -- **Command Palette**: Quick access to settings, slash commands, and actions (Ctrl+P) -- **Interactive Settings**: Live configuration of auto-approval, model parameters, and system behavior -- **Slash Commands**: Built-in commands for common workflows (`/commit`, `/pr`, `/clear`, `/help`) -- **Real-time Streaming**: See agent thinking, tool calls, and responses as they happen +- **Real-time Streaming**: See agent thinking, tool calls, and responses stream in as they happen +- **Compact Header Bar**: Workspace path and active model shown at a glance; context-aware hints update with TUI state +- **Smart Scroll-Lock**: Scroll up to review history while the agent works; a banner appears when new content arrives, press `G` to jump back and resume auto-follow +- **Command Palette**: Instant slash command launcher via `Ctrl+K` / `Ctrl+P` or typing `/`; `Enter` executes immediately +- **Diff Viewer**: Interactive unified diff viewer with syntax highlighting for file changes +- **Tool Result History**: Browse all tool results from the session with `Ctrl+L`; inspect the latest result with `Ctrl+V` +- **Clipboard Copy**: Copy the full conversation as plain text with `Ctrl+Y` +- **Agent Thinking Blocks**: Extended reasoning shown inline with elapsed time; toggle visibility in settings +- **Interactive Settings**: Live configuration of LLM parameters, auto-approval rules, and UI preferences +- **Slash Commands**: Built-in commands for common workflows (`/commit`, `/pr`, `/bash`, `/notes`, `/help`, `/snapshot`) ### πŸ› οΈ Complete Coding Toolkit @@ -130,6 +134,7 @@ func main() { - [Understanding the Agent Loop](docs/getting-started/understanding-agent-loop.md) - Core concepts ### How-To Guides +- [Use TUI Interface](docs/how-to/use-tui-interface.md) - Full TUI guide (scroll-lock, clipboard, thinking blocks, overlays) - [Configure Provider](docs/how-to/configure-provider.md) - LLM provider setup - [Create Custom Tools](docs/how-to/create-custom-tool.md) - Extend agent capabilities - [Setup PR Documentation](docs/how-to/setup-pr-documentation.md) - Automated docs workflow @@ -157,9 +162,11 @@ func main() { ### Architecture Decision Records See [ADRs](docs/adr/) for detailed design decisions including: +- [TUI Visual Redesign](docs/adr/0051-tui-visual-redesign.md) +- [TUI Clipboard Copy](docs/adr/0050-tui-clipboard-copy.md) +- [TUI Bracketed Paste](docs/adr/0049-tui-bracketed-paste-support.md) +- [TUI Smart Scroll-Lock](docs/adr/0048-tui-smart-scroll-lock.md) - [Automated PR Documentation](docs/adr/0030-automated-pr-documentation.md) -- [Headless Git PR Creation](docs/adr/0031-headless-git-pr-creation.md) -- [Headless Git Integration](docs/adr/0029-headless-git-integration.md) - [XML Tool Call Format](docs/adr/0019-xml-cdata-tool-call-format.md) - [Auto-Approval System](docs/adr/0017-auto-approval-and-settings-system.md) - [Context Management](docs/adr/0014-composable-context-management.md) diff --git a/cmd/forge/main.go b/cmd/forge/main.go index 9367bd7..7a52860 100644 --- a/cmd/forge/main.go +++ b/cmd/forge/main.go @@ -24,6 +24,7 @@ import ( "github.com/entrhq/forge/pkg/executor/tui" "github.com/entrhq/forge/pkg/llm" "github.com/entrhq/forge/pkg/llm/openai" + frameworkVersion "github.com/entrhq/forge/pkg/version" "github.com/entrhq/forge/pkg/security/workspace" "github.com/entrhq/forge/pkg/tools/browser" @@ -33,7 +34,7 @@ import ( ) const ( - version = "0.1.0" // Version of the Forge coding agent + version = frameworkVersion.Version // Version of the Forge coding agent defaultModel = "anthropic/claude-sonnet-4.5" // Default model to use // Context management defaults for coding sessions diff --git a/docs/adr/0010-tool-approval-mechanism.md b/docs/adr/0010-tool-approval-mechanism.md index 57e0a87..2fbc387 100644 --- a/docs/adr/0010-tool-approval-mechanism.md +++ b/docs/adr/0010-tool-approval-mechanism.md @@ -236,8 +236,8 @@ For example, `ApplyDiffTool.Preview()` returns formatted diff output. - [ADR-0008](0008-agent-controlled-loop-termination.md) - Agent loop control flow - [ADR-0009](0009-tui-executor-design.md) - TUI executor architecture -- ADR-0011 (upcoming) - Coding tools architecture -- ADR-0012 (upcoming) - Enhanced TUI diff viewer +- [ADR-0011](0011-coding-tools-architecture.md) - Coding tools architecture +- [ADR-0012](0012-enhanced-tui-executor.md) - Enhanced TUI diff viewer --- diff --git a/docs/adr/0011-coding-tools-architecture.md b/docs/adr/0011-coding-tools-architecture.md index 41b4b4c..41e81ea 100644 --- a/docs/adr/0011-coding-tools-architecture.md +++ b/docs/adr/0011-coding-tools-architecture.md @@ -329,7 +329,7 @@ Tools like `ApplyDiffTool` and `WriteFileTool` implement this to show diffs/chan ## Related Decisions - [ADR-0010](0010-tool-approval-mechanism.md) - Tool approval flow -- ADR-0012 (upcoming) - Enhanced TUI with diff viewer +- [ADR-0012](0012-enhanced-tui-executor.md) - Enhanced TUI with diff viewer - [ADR-0009](0009-tui-executor-design.md) - TUI executor architecture --- diff --git a/docs/adr/0012-enhanced-tui-executor.md b/docs/adr/0012-enhanced-tui-executor.md index 344c9f2..c4b63cc 100644 --- a/docs/adr/0012-enhanced-tui-executor.md +++ b/docs/adr/0012-enhanced-tui-executor.md @@ -1,6 +1,6 @@ # 12. Enhanced TUI Executor with Diff Viewer -**Status:** Proposed +**Status:** Implemented **Date:** 2025-01-05 **Deciders:** Forge Core Team **Technical Story:** Extending the TUI executor to support coding workflows with diff preview, file navigation, and command output display diff --git a/docs/adr/0025-tui-package-reorganization.md b/docs/adr/0025-tui-package-reorganization.md index e896c41..73df441 100644 --- a/docs/adr/0025-tui-package-reorganization.md +++ b/docs/adr/0025-tui-package-reorganization.md @@ -1,6 +1,6 @@ # 0025. TUI Package Reorganization (Interface-Driven Design) -**Status:** Proposed +**Status:** Implemented **Date:** 2024-06-17 **Deciders:** Developer, Architect **Technical Story:** Refactoring the monolithic `pkg/executor/tui` package into maintainable subpackages. diff --git a/docs/adr/0033-notes-viewer-tui-command.md b/docs/adr/0033-notes-viewer-tui-command.md index 7e11d89..932b710 100644 --- a/docs/adr/0033-notes-viewer-tui-command.md +++ b/docs/adr/0033-notes-viewer-tui-command.md @@ -1,6 +1,6 @@ # 0033. Notes Viewer TUI Command -**Status:** Proposed +**Status:** Implemented **Date:** 2024-12-19 **Deciders:** Engineering Team **Technical Story:** Implementation of `/notes` slash command to view agent scratchpad notes in TUI diff --git a/docs/adr/0051-tui-visual-redesign.md b/docs/adr/0051-tui-visual-redesign.md index 4b7acff..b02c4ab 100644 --- a/docs/adr/0051-tui-visual-redesign.md +++ b/docs/adr/0051-tui-visual-redesign.md @@ -1,6 +1,6 @@ # 51. TUI Visual Redesign -**Status:** Proposed +**Status:** Implemented **Date:** 2025-01-26 **Deciders:** Product Team, Engineering Team **Technical Story:** [TUI Visual Polish PRD](../product/features/tui-visual-polish.md) β€” compact header and input box design @@ -126,7 +126,7 @@ Option 2 delivers the full visual improvement with a contained, reviewable diff. ### Neutral -- `headerHeight` changes from `10` to `3` β€” must be updated in one place (`update.go:319`) +- `headerHeight` changes from `10` to `4` β€” must be updated in one place (`calculateViewportHeight()` in `update.go`) - `buildHeader()` function is replaced in-place; callers do not change --- @@ -140,47 +140,65 @@ Replace `buildHeader()` in `pkg/executor/tui/view.go`: ```go // buildHeader renders the compact single-line header bar. // Total height: 2 lines (bar + separator). -func (m model) buildHeader() string { - // Truncate working directory to fit in available width - cwd := m.workingDir +func (m *model) buildHeader() string { + // Resolve model name from the LLM provider at render time. + modelName := "" + if m.provider != nil { + modelName = m.provider.GetModel() + } + + // Truncate working directory to fit in available width. + // Uses m.workspaceDir β€” the field name on the model struct. + cwd := m.workspaceDir + if abs, err := filepath.Abs(cwd); err == nil { + cwd = abs + } if m.width > 0 && lipgloss.Width(cwd) > m.width/2 { cwd = "…" + cwd[len(cwd)-(m.width/2):] } - left := m.styles.brandStyle.Render("⬑ forge") - mid := m.styles.mutedStyle.Render(cwd) - right := m.styles.mutedStyle.Render(m.modelName + " " + version.Version) + // Package-level style vars β€” no m.styles struct exists. + left := headerStyle.Render("⬑ forge") + mid := tipsStyle.Render(cwd) + right := tipsStyle.Render(modelName + " v" + version.Version) - // Fill space between left, mid, right + // Fill space between left, mid, right. totalUsed := lipgloss.Width(left) + lipgloss.Width(mid) + lipgloss.Width(right) gap := (m.width - totalUsed) / 2 if gap < 1 { gap = 1 } pad := strings.Repeat(" ", gap) bar := left + pad + mid + pad + right - separator := m.styles.dimStyle.Render(strings.Repeat("─", m.width)) + separator := inputRuleStyle.Render(strings.Repeat("─", m.width)) return bar + "\n" + separator } ``` -**`pkg/executor/tui/update.go:319`** β€” update `headerHeight`: +**`pkg/executor/tui/update.go`** β€” update `headerHeight` in `calculateViewportHeight()`: ```go -// headerHeight is the number of lines occupied by buildHeader() output. -// Compact header: 1 line bar + 1 line separator = 2 lines. -// Plus 1 line for the contextual hints = 3 total. -headerHeight := 3 +// headerHeight is the number of lines occupied by chrome above the viewport. +// Breakdown (see assembleBaseView): +// header = buildHeader() = 2 lines (bar + separator) +// tips = buildTips() = 1 line +// spacer = blank "" = 1 line +// Total = 4. Static β€” never grows. +const headerHeight = 4 ``` ### Step 2 β€” Option B input box (styles.go + view.go, ~20 lines) -**`pkg/executor/tui/styles.go`** β€” replace `inputBoxStyle` with `inputRuleStyle`: +**`pkg/executor/tui/styles.go`** β€” add `dimSep` color and `inputRuleStyle`: ```go -// inputRuleStyle renders the horizontal rule above the input field (Option B). +// Color additions +dimSep = lipgloss.Color("#374151") // dim separator β€” slightly lighter than bg, used for rules/dividers + +// inputRuleStyle renders the horizontal rule above the input field and +// the header separator (Option B design, ADR-0051 Steps 2 & 5). +// Uses dimSep (#374151) β€” darker than mutedGray so the rule recedes visually. inputRuleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(mutedGray)). - Width(0) // width set dynamically in buildInputBox + Foreground(dimSep) // inputPromptStyle styles the ❯ glyph inputPromptStyle = lipgloss.NewStyle(). @@ -225,32 +243,37 @@ height on every recalculation β€” not a fixed number. ```go func (m *model) calculateViewportHeight() int { - // headerHeight: bar + separator + hints line. Static β€” never grows. - const headerHeight = 3 - - // inputZoneHeight is dynamic: rule (1) + live textarea lines + hints (1). - // Use live line count from the textarea value; cap at height/3 so a very - // long draft cannot reduce the viewport to zero. + // headerHeight: bar (1) + separator (1) + tips (1) + blank spacer (1) = 4 lines. + // These are the rows prepended by assembleBaseView before the viewport. + // Static β€” never grows with content. + const headerHeight = 4 + + // inputZoneHeight is dynamic: rule (1) + live textarea lines. + // Tips are already counted in headerHeight (rendered by buildTips() above). + // Use strings.Count on the actual textarea value so the height is in sync + // with content in the same tick β€” avoids the one-frame lag from textarea.Height() + // which reflects the allocated component height, not live content lines. liveLines := strings.Count(m.textarea.Value(), "\n") + 1 - if liveLines < 1 { - liveLines = 1 - } - maxInputLines := m.height / 3 - if maxInputLines < 1 { - maxInputLines = 1 - } - if liveLines > maxInputLines { - liveLines = maxInputLines - } - inputZoneHeight := 1 + liveLines + 1 // rule + textarea + hints + inputZoneHeight := 1 + liveLines // rule + live content lines const statusBarHeight = 1 - available := m.height - headerHeight - inputZoneHeight - statusBarHeight - if available < 1 { - available = 1 + // Reserve one line for the loading spinner when the agent is busy. + loadingHeight := 0 + if m.agentBusy { + loadingHeight = 1 } - return available + // ADR-0048: reserve one line for the scroll-lock "new content" indicator. + scrollIndicatorHeight := 0 + if !m.followScroll && m.hasNewContent { + scrollIndicatorHeight = 1 + } + + viewportHeight := m.height - headerHeight - inputZoneHeight - statusBarHeight - loadingHeight - scrollIndicatorHeight + if viewportHeight < 5 { + viewportHeight = 5 + } + return viewportHeight } ``` @@ -258,13 +281,15 @@ func (m *model) calculateViewportHeight() int { changes. In the textarea update branch of `Update()`: ```go -prevLines := strings.Count(m.textarea.Value(), "\n") -newTextarea, cmd := m.textarea.Update(msg) -m.textarea = newTextarea -if strings.Count(m.textarea.Value(), "\n") != prevLines { +// Compare live line count before/after update so calculateViewportHeight() +// always reads the latest content (ADR-0051 Β§2b). +oldLines := strings.Count(m.textarea.Value(), "\n") + 1 +m.textarea, tiCmd = m.textarea.Update(msg) +newLines := strings.Count(m.textarea.Value(), "\n") + 1 + +if oldLines != newLines && m.ready { m.recalculateLayout() } -return m, cmd ``` This ensures the viewport shrinks when the user types a multiline draft and expands back when they @@ -295,26 +320,31 @@ Replace the static `buildTips()` function with a state-aware version: ```go // buildTips returns a single-line hints string adapted to the current TUI state. -func (m model) buildTips() string { - muted := m.styles.mutedStyle - +// Note: uses package-level tipsStyle (no m.styles struct); checks m.overlay.isActive() +// not m.overlay != nil (overlayState always exists, may be inactive); uses m.agentBusy +// not m.agentRunning; includes a bashMode case for the bash REPL sub-mode. +func (m *model) buildTips() string { switch { - case m.overlay != nil: - // An overlay is open β€” show overlay-specific hints - return muted.Render(" Esc Β· close Tab Β· next field Enter Β· confirm") + case m.overlay.isActive(): + // An overlay is open β€” show overlay-specific hints. + return tipsStyle.Render(" Esc Β· close Tab Β· next field Enter Β· confirm") - case m.agentRunning: - // Agent is active + case m.agentBusy: + // Agent is active β€” show interrupt hint. hints := " Ctrl+C Β· interrupt" if !m.followScroll { hints += " G Β· follow output" } - return muted.Render(hints) + return tipsStyle.Render(hints) + + case m.bashMode: + // Bash REPL sub-mode β€” show bash-specific hints. + return tipsStyle.Render(" Enter Β· run exit Β· return to normal Ctrl+C Β· cancel") default: - // Idle - return muted.Render( - " Enter Β· send Alt+Enter Β· new line / Β· commands Ctrl+Y Β· copy Ctrl+C", + // Idle β€” show full send/command hints. + return tipsStyle.Render( + " Enter Β· send Alt+Enter Β· new line / Β· commands Ctrl+Y Β· copy Ctrl+C Β· exit", ) } } @@ -325,15 +355,18 @@ func (m model) buildTips() string { Add any missing style constants used above: ```go -const ( +var ( // existing - salmonPink = "#FFB3BA" - mutedGray = "#6B7280" - // new - dimSeparator = "#374151" // slightly lighter than background for rule/separator + salmonPink = lipgloss.Color("#FFB3BA") + mutedGray = lipgloss.Color("#6B7280") + // new β€” dim separator, slightly lighter than background for rules and dividers + dimSep = lipgloss.Color("#374151") ) ``` +> **Note:** The constant in code is named `dimSep` (not `dimSeparator`). All color vars are +> `lipgloss.Color` values (not raw strings), declared in a `var` block in `styles.go`. + ### Migration Path `pkg/ui/ascii.go` is retained. `GenerateASCIIArt()` is no longer called from `view.go:buildHeader()` but is not deleted β€” it may be used in other contexts (CLI startup banner, help text). Remove the call from `buildHeader()`; do not delete the function. diff --git a/docs/adr/0052-tui-viewport-spacer-height-fix.md b/docs/adr/0052-tui-viewport-spacer-height-fix.md new file mode 100644 index 0000000..759cfb3 --- /dev/null +++ b/docs/adr/0052-tui-viewport-spacer-height-fix.md @@ -0,0 +1,318 @@ +# 52. TUI Viewport Spacer Height Fix + +**Status:** Implemented +**Date:** 2025-01-30 +**Deciders:** Engineering Team +**Technical Story:** Fix viewport height calculation to account for visual spacer line in `assembleBaseView` + +--- + +## Context + +After the TUI visual redesign (ADR-0051) which replaced the 6-line ASCII art header with a compact single-line header bar, users reported a persistent layout bug: blank space appeared below the TUI viewport, with the amount of blank space proportional to the window resize delta. The bug only manifested when: + +1. User resized the terminal window to make it smaller (height reduction) +2. User then pressed space or any key that triggered textarea content changes + +The blank space would remain until the next layout recalculation was forced by another resize or viewport scroll event. + +### Background + +The TUI layout is assembled in `pkg/executor/tui/view.go` via `assembleBaseView()`, which joins multiple sections vertically: + +```go +func (m *model) assembleBaseView(header, tips, viewportSection, scrollIndicator, loadingIndicator, inputBox, bottomBar string) string { + var middle []string + middle = append(middle, viewportSection) + if scrollIndicator != "" { + middle = append(middle, scrollIndicator) + } + if m.agentBusy { + middle = append(middle, loadingIndicator) + } + + // Visual spacer between header and content + rows := []string{header, tips, ""} + rows = append(rows, middle...) + rows = append(rows, inputBox, bottomBar) + + result := lipgloss.JoinVertical(lipgloss.Left, rows...) + return result +} +``` + +Note line 231: `rows := []string{header, tips, ""}` β€” a blank string `""` is inserted as a visual spacer between the tips line and the viewport content. + +The viewport height is calculated in two places in `pkg/executor/tui/update.go`: + +1. Inside `Update()` when detecting textarea content changes (inline calculation) +2. In the dedicated `calculateViewportHeight()` function called during layout recalculation + +Both calculations used this formula: + +```go +headerHeight := 2 // header line + tips line +inputZoneHeight := 1 + strings.Count(m.textarea.Value(), "\n") + 1 +statusBarHeight := 1 +loadingHeight := 0 // or 1 if agentBusy +scrollIndicatorHeight := 0 // or 1 if scroll lock active + +viewportHeight := m.height - headerHeight - inputZoneHeight - statusBarHeight - loadingHeight - scrollIndicatorHeight +``` + +**The bug:** This formula never accounted for the visual spacer line. The viewport was allocated 1 extra line beyond what the terminal could actually display. + +### Problem Statement + +The blank spacer row inserted at `view.go:231` consumed 1 line of vertical space but was never subtracted from the viewport height budget in either calculation site. This caused a cumulative off-by-one error during rapid window resize sequences: + +1. Each `WindowSizeMsg` event called `handleWindowResize()`, which set `m.height` and `m.width` +2. `calculateViewportHeight()` was called, returning a viewport height that was 1 line too large +3. `GotoBottom()` was called with the incorrect height, setting `YOffset` based on wrong dimensions +4. The next resize event arrived before the layout could stabilize, compounding the error +5. More resize events = more compounding = larger final gap + +The bug appeared proportional to resize delta because: +- Small resize (2-3 lines) = 2-3 resize events = 2-3 compounded errors = 2-3 blank lines +- Large resize (5+ lines) = 5+ resize events = 5+ compounded errors = 5+ blank lines + +Debug logs confirmed that after resizing from height=20 to height=16 (4 steps), `YOffset=3` β€” consistent with 3 compounding errors across a 4-event sequence. + +### Goals + +- Fix the viewport height calculation to account for the visual spacer line +- Apply the fix to both calculation sites (inline in `Update()` and `calculateViewportHeight()`) +- Ensure the layout remains stable during rapid resize sequences +- Remove all temporary debug logging added during investigation + +### Non-Goals + +- Removing the visual spacer (it serves a legitimate UX purpose) +- Refactoring the dual-calculation-site architecture (separate concern) +- Changing the `assembleBaseView()` row assembly order + +--- + +## Decision Drivers + +* **Correctness** β€” The viewport height formula must match the actual vertical space consumed by all UI elements +* **Maintainability** β€” The fix must be obvious to future maintainers (both the constant declaration and the comment explaining its origin) +* **Consistency** β€” Both calculation sites must use the same formula +* **Evidence-based** β€” The fix must be confirmed via debug logs before being applied + +--- + +## Considered Options + +### Option 1: Remove the visual spacer + +**Description:** Delete the blank string from the `rows` array in `assembleBaseView()`. + +**Pros:** +- Eliminates the accounting problem entirely +- Simpler layout code + +**Cons:** +- Degrades UX β€” the spacer provides visual breathing room between tips and content +- Contradicts the visual design from ADR-0051 + +### Option 2: Add `spacerHeight = 1` constant + +**Description:** Declare `const spacerHeight = 1` in `calculateViewportHeight()` and subtract it from the viewport height budget. Add an inline comment referencing the `assembleBaseView` line number. + +**Pros:** +- Minimal change β€” just adds one constant and one subtraction +- Self-documenting via constant name and comment +- Preserves visual design +- Easy to verify via log analysis + +**Cons:** +- Requires applying the fix to two separate calculation sites +- Constant must be kept in sync with `assembleBaseView()` implementation + +### Option 3: Dynamically measure spacer height + +**Description:** Count the number of empty strings in the `rows` array at runtime and adjust viewport height accordingly. + +**Pros:** +- No hardcoded constant to maintain +- Automatically adapts to layout changes + +**Cons:** +- Adds runtime overhead to every layout calculation +- Couples viewport height calculation to view assembly internals +- Overly complex for a single blank line + +--- + +## Decision + +**Chosen Option:** Option 2 β€” Add `spacerHeight = 1` constant + +### Rationale + +The visual spacer serves a legitimate UX purpose and was explicitly introduced in ADR-0051. Removing it (Option 1) would degrade the visual design. Dynamic measurement (Option 3) adds unnecessary complexity for a single static line. + +Option 2 is the minimal fix that preserves the visual design while being self-documenting. The constant name `spacerHeight` clearly communicates its purpose, and the inline comment linking to `assembleBaseView` line 231 ensures future maintainers understand the dependency. + +The fix was verified via debug logs before being applied: +- Debug logging confirmed `YOffset` incremented by 1 on each resize-smaller event +- After applying the fix, build succeeded and user confirmed the bug was resolved +- All debug logging was then removed + +--- + +## Consequences + +### Positive + +- Viewport layout is now stable during rapid resize sequences +- No blank space appears below the TUI after resize + keypress +- The fix is self-documenting via constant name and comment +- Visual design from ADR-0051 is preserved + +### Negative + +- The `spacerHeight` constant must be manually updated if `assembleBaseView()` changes its spacer implementation +- The fix exists in two separate calculation sites that must be kept in sync + +### Neutral + +- One additional constant declaration in `calculateViewportHeight()` +- One additional subtraction in the viewport height formula at both sites + +--- + +## Implementation + +The fix was applied to `pkg/executor/tui/update.go` at two sites: + +### Site 1: Inline calculation in `Update()` (around line 140) + +```go +headerHeight := 2 +inputZoneHeight := 1 + strings.Count(m.textarea.Value(), "\n") + 1 +statusBarHeight := 1 +loadingHeight := 0 +if m.agentBusy { + loadingHeight = 1 +} +scrollIndicatorHeight := 0 +if !m.followScroll && m.hasNewContent { + scrollIndicatorHeight = 1 +} +// Visual spacer line between header and viewport (assembleBaseView line 231 adds "") +const spacerHeight = 1 + +newVpHeight := m.height - headerHeight - spacerHeight - inputZoneHeight - statusBarHeight - loadingHeight - scrollIndicatorHeight +``` + +### Site 2: `calculateViewportHeight()` function (around line 289) + +```go +func (m *model) calculateViewportHeight() int { + headerHeight := 2 + inputZoneHeight := 1 + strings.Count(m.textarea.Value(), "\n") + 1 + statusBarHeight := 1 + + loadingHeight := 0 + if m.agentBusy { + loadingHeight = 1 + } + + scrollIndicatorHeight := 0 + if !m.followScroll && m.hasNewContent { + scrollIndicatorHeight = 1 + } + + // Visual spacer line between header and viewport (assembleBaseView line 231 adds "") + const spacerHeight = 1 + + viewportHeight := m.height - headerHeight - spacerHeight - inputZoneHeight - statusBarHeight - loadingHeight - scrollIndicatorHeight + if viewportHeight < 1 { + viewportHeight = 1 + } + return viewportHeight +} +``` + +Both sites now include: +1. `const spacerHeight = 1` declaration +2. Inline comment referencing `assembleBaseView` line 231 +3. Subtraction of `spacerHeight` in the formula + +### Migration Path + +No migration required β€” this is a runtime layout calculation fix. Existing TUI sessions will automatically pick up the corrected layout on the next resize or recalculation event. + +### Cleanup + +All temporary debug logging added during investigation was removed: +- `pkg/executor/tui/update.go`: Removed 5 `debugLog.Debugf` calls (UPDATE-START, KEY-DEBUG, TEXTAREA-UPDATE, UPDATE-END, RESIZE) +- `pkg/executor/tui/events.go`: Removed 7 debug logs from `scrollToBottomOrMark()` +- `pkg/executor/tui/view.go`: Removed 4 debug blocks (VIEWPORT-VIEW, FINAL-VIEW, INPUT-BOX, VIEW assembleBaseView) + +--- + +## Validation + +### Success Metrics + +1. No blank space appears below the TUI after window resize + keypress +2. Layout remains stable during rapid resize sequences (20+ events/second) +3. `make build` succeeds with no compilation errors +4. No debug logging remains in production code + +### Verification + +User confirmed via interactive testing: +- Resized terminal window from 25 lines to 15 lines (10-line delta) +- Pressed space key after resize +- No blank space appeared below the viewport +- Layout remained stable during subsequent interaction + +Build verification: +```bash +make build +# Output: βœ“ forge built successfully at .bin/forge +``` + +Code search verification: +```bash +rg "debugLog\." pkg/executor/tui/ +# Output: No matches found +``` + +--- + +## Related Decisions + +- [ADR-0051](0051-tui-visual-redesign.md) β€” TUI Visual Redesign (introduced the compact header and visual spacer) +- [ADR-0025](0025-tui-package-reorganization.md) β€” TUI Package Reorganization (established view/update separation) + +--- + +## References + +- `pkg/executor/tui/view.go:231` β€” Visual spacer insertion in `assembleBaseView()` +- `pkg/executor/tui/update.go:302` β€” `calculateViewportHeight()` implementation +- `pkg/executor/tui/update.go:~140` β€” Inline viewport height calculation in `Update()` + +--- + +## Notes + +**Why the bug appeared proportional to resize delta:** + +The bug appeared proportional because of cascading errors during rapid resize sequences. Each `WindowSizeMsg` event: +1. Called `calculateViewportHeight()` which returned a height 1 line too large +2. Called `GotoBottom()` with incorrect height, setting `YOffset` based on wrong dimensions +3. The next event arrived before layout could stabilize, compounding the error + +More resize events = more compounding = larger final gap. This is why a 2-line resize produced 2 blank lines, and a 5-line resize produced 5 blank lines. + +**Why `m.textarea.Height()` was replaced:** + +The original inline calculation used `m.textarea.Height()` which returns the previous frame's allocated component height, not the live content line count. This caused a one-frame lag artifact. The fix uses `strings.Count(m.textarea.Value(), "\n") + 1` for same-tick accuracy. + +**Last Updated:** 2025-01-30 diff --git a/docs/adr/README.md b/docs/adr/README.md index fd9aaa3..ed520f5 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -73,7 +73,7 @@ Proposed β†’ Accepted β†’ [Deprecated or Superseded] | [0009](0009-tui-executor-design.md) | TUI Executor Design | Accepted | | [0010](0010-tool-approval-mechanism.md) | Tool Approval Mechanism | Accepted | | [0011](0011-coding-tools-architecture.md) | Coding Tools Architecture | Accepted | -| [0012](0012-enhanced-tui-executor.md) | Enhanced TUI Executor | Accepted | +| [0012](0012-enhanced-tui-executor.md) | Enhanced TUI Executor | Implemented | | [0013](0013-streaming-command-execution.md) | Streaming Command Execution with Interactive Overlay | Accepted | | [0014](0014-composable-context-management.md) | Composable Context Management with Strategy Pattern | Proposed | | [0015](0015-buffered-tool-call-summarization.md) | Buffered Tool Call Summarization with Parallel Processing | Accepted | @@ -86,7 +86,7 @@ Proposed β†’ Accepted β†’ [Deprecated or Superseded] | [0022](0022-intelligent-tool-result-display.md) | Intelligent Tool Result Display | Accepted | | [0023](0023-bash-mode-architecture.md) | Bash Mode Architecture | Accepted | | [0024](0024-xml-escaping-primary-with-cdata-fallback.md) | XML Escaping Primary with CDATA Fallback | Accepted | -| [0025](0025-tui-package-reorganization.md) | TUI Package Reorganization | Accepted | +| [0025](0025-tui-package-reorganization.md) | TUI Package Reorganization | Implemented | | [0026](0026-headless-mode-architecture.md) | Headless Mode Architecture | Accepted | | [0027](0027-safety-constraint-system.md) | Safety Constraint System | Accepted | | [0028](0028-quality-gate-architecture.md) | Quality Gate Architecture | Accepted | @@ -94,7 +94,7 @@ Proposed β†’ Accepted β†’ [Deprecated or Superseded] | [0030](0030-automated-pr-documentation.md) | Automated PR Documentation | Accepted | | [0031](0031-headless-git-pr-creation.md) | Headless Git PR Creation | Accepted | | [0032](0032-agent-scratchpad-notes-system.md) | Agent Scratchpad Notes System | Accepted | -| [0033](0033-notes-viewer-tui-command.md) | Notes Viewer TUI Command | Accepted | +| [0033](0033-notes-viewer-tui-command.md) | Notes Viewer TUI Command | Implemented | | [0034](0034-live-reloadable-llm-settings.md) | Live-Reloadable LLM Settings | Accepted | | [0035](0035-auto-close-command-overlay.md) | Auto-Close Command Overlay | Accepted | | [0036](0036-agents-md-repository-context.md) | AGENTS.md Repository Context | Accepted | @@ -109,10 +109,10 @@ Proposed β†’ Accepted β†’ [Deprecated or Superseded] | [0045](0045-long-term-memory-embedding-provider.md) | Long-Term Memory Embedding Provider | Accepted | | [0046](0046-long-term-memory-capture.md) | Long-Term Memory Capture | Accepted | | [0047](0047-long-term-memory-retrieval.md) | Long-Term Memory Retrieval | Accepted | -| [0048](0048-tui-smart-scroll-lock.md) | TUI Smart Scroll-Lock | Proposed | -| [0049](0049-tui-bracketed-paste-support.md) | TUI Bracketed Paste Support | Proposed | -| [0050](0050-tui-clipboard-copy.md) | TUI Clipboard Copy | Proposed | -| [0051](0051-tui-visual-redesign.md) | TUI Visual Redesign | Proposed | +| [0048](0048-tui-smart-scroll-lock.md) | TUI Smart Scroll-Lock | Implemented | +| [0049](0049-tui-bracketed-paste-support.md) | TUI Bracketed Paste Support | Implemented | +| [0050](0050-tui-clipboard-copy.md) | TUI Clipboard Copy | Implemented | +| [0051](0051-tui-visual-redesign.md) | TUI Visual Redesign | Implemented | ## Resources diff --git a/docs/how-to/use-tui-interface.md b/docs/how-to/use-tui-interface.md index 045d23e..1b53f40 100644 --- a/docs/how-to/use-tui-interface.md +++ b/docs/how-to/use-tui-interface.md @@ -1,7 +1,7 @@ # How to Use the TUI Interface -**Last Updated:** December 2024 -**Difficulty:** Beginner +**Last Updated:** January 2025 +**Difficulty:** Beginner **Estimated Time:** 10 minutes --- @@ -15,13 +15,17 @@ The Forge Terminal User Interface (TUI) provides an interactive chat-based inter ## Table of Contents 1. [Starting the TUI](#starting-the-tui) -2. [Basic Chat Interface](#basic-chat-interface) -3. [Keyboard Shortcuts](#keyboard-shortcuts) -4. [Slash Commands](#slash-commands) -5. [Overlays](#overlays) -6. [Tool Approval Workflow](#tool-approval-workflow) -7. [Settings Configuration](#settings-configuration) -8. [Tips & Best Practices](#tips--best-practices) +2. [Interface Layout](#interface-layout) +3. [Basic Chat Interface](#basic-chat-interface) +4. [Keyboard Shortcuts](#keyboard-shortcuts) +5. [Smart Scroll-Lock](#smart-scroll-lock) +6. [Clipboard Copy](#clipboard-copy) +7. [Slash Commands](#slash-commands) +8. [Overlays](#overlays) +9. [Agent Thinking Blocks](#agent-thinking-blocks) +10. [Tool Approval Workflow](#tool-approval-workflow) +11. [Settings Configuration](#settings-configuration) +12. [Tips & Best Practices](#tips--best-practices) --- @@ -39,17 +43,87 @@ Or if running from source: go run cmd/forge/main.go tui ``` -### Welcome Screen +The TUI opens directly into the main interface β€” there is no splash screen. -Upon first launch, a welcome screen is displayed with ASCII art branding. This screen confirms that the application has started successfully. Press any key to proceed to the main interface. +--- + +## Interface Layout + +The TUI is composed of five visual zones stacked vertically: + +``` +⬑ forge /path/to/workspace gpt-4o +──────────────────────────────────────────────────── + Enter Β· send Alt+Enter Β· new line / Β· commands Ctrl+Y Β· copy Ctrl+C Β· exit + + β”Œ conversation viewport ─────────────────────────┐ + β”‚ β”‚ + β”‚ [agent messages, tool calls, results appear β”‚ + β”‚ here and scroll as the conversation grows] β”‚ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +↓ New content below β€” press G or PgDn to follow ← only when scroll-locked + + [loading spinner + message] ← only when agent is busy +──────────────────────────────────────────────────── +❯ [your input here] + βΈ« Thinking On ctx β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘ 12k / 128k +``` + +### Header Bar (2 lines) + +- **Left**: `⬑ forge` β€” brand identifier (salmonPink) +- **Center**: Current workspace directory (truncated if too wide) +- **Right**: Active LLM model name (e.g. `gpt-4o`) +- **Separator**: Full-width `─` rule beneath the bar + +### Hints Bar + +A single line of contextual keyboard hints that changes based on current state: + +| State | Hints shown | +|-------|-------------| +| Idle | `Enter Β· send Alt+Enter Β· new line / Β· commands Ctrl+Y Β· copy Ctrl+C Β· exit` | +| Agent busy | `Ctrl+C Β· interrupt` (+ `G Β· follow output` when scroll-locked) | +| Overlay open | `Esc Β· close Tab Β· next field Enter Β· confirm` | +| Bash mode | `Enter Β· run exit Β· return to normal Ctrl+C Β· cancel` | + +### Conversation Viewport + +Scrollable area displaying the full conversation history. New content streams in at the bottom. When the viewport is auto-following output, the view scrolls down automatically as the agent responds. + +### Scroll-Lock Indicator -### Main Interface +When you scroll up while the agent is generating output, a banner appears: -After the welcome screen, the main interface appears. It is composed of several key components: -- **Header**: Shows workspace path and context information. -- **Chat Area**: Displays the conversation history with the agent. -- **Input Box**: Where you type messages (at the bottom of the screen). -- **Status Bar**: Shows the current agent state, token usage, and contextual hints. +``` +↓ New content below β€” press G or PgDn to follow +``` + +Press **G** or **PgDn** to jump back to the bottom and resume auto-following. + +### Input Zone + +A borderless input area with a `❯` prompt glyph: + +``` +───────────────────────────────────────────────── +❯ type your message here +``` + +The textarea grows automatically as you type multi-line content (up to one-third of the screen height). + +### Status Bar + +The bottom status bar shows: + +- **Left**: `bash mode` label (only visible in bash mode, in mintGreen) +- **Right**: Thinking state indicator (`βΈ« Thinking On` / `βΈ« Thinking Hidden`) and context usage bar + +The context bar format: `ctx β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘ 12k / 128k` +- Bar fills proportionally to current context usage +- Color changes from green β†’ orange β†’ red as context fills up --- @@ -59,7 +133,7 @@ After the welcome screen, the main interface appears. It is composed of several 1. **Type your message** in the input box at the bottom 2. **Press Enter** to send the message to the agent -3. The agent will process your request and respond +3. The agent will process your request and respond in real time **Example conversation:** ``` @@ -67,22 +141,22 @@ You: Create a new file called hello.go with a simple main function Agent: I'll create that file for you. -[Tool Call: write_file] -- path: hello.go -- content: package main... +[write_file] hello.go + + package main... -βœ“ File created successfully +[βœ“] File created successfully ``` ### Message Types The chat interface displays different message types with visual indicators: -- **User Messages**: Your input (left-aligned) -- **Agent Messages**: Agent responses (left-aligned, different color) -- **Tool Calls**: Actions the agent wants to take (highlighted) -- **Tool Results**: Outcome of tool executions (with status icons) -- **System Messages**: Status updates and notifications +- **Your messages**: Your input, labeled `You:` +- **Agent messages**: Agent prose responses +- **Thinking blocks**: Extended reasoning (shown/hidden based on the thinking toggle β€” see [Agent Thinking Blocks](#agent-thinking-blocks)) +- **Tool calls**: Actions the agent is taking, shown as the tool name and parameters +- **Tool results**: Outcome of tool executions, summarized with status icons +- **System messages**: Status updates and toast notifications ### Multi-line Input @@ -91,66 +165,116 @@ To add line breaks in your message: - Continue typing on the next line - **Press Enter** (without Alt) to send the complete message +The input area grows automatically to accommodate multiple lines. + +### Pasting Text + +Standard paste shortcuts work in the input area: +- **Cmd+V** (macOS) or **Ctrl+Shift+V** / **Shift+Insert** (Linux) + +In the settings overlay, bracketed paste (ADR-0049) is supported β€” pasted text is treated as literal input rather than individual keystrokes. + --- ## Keyboard Shortcuts -### Essential Shortcuts +### Global Shortcuts + +| Shortcut | Action | +|----------|--------| +| **Enter** | Send message | +| **Alt+Enter** | Insert new line | +| **Ctrl+C** | Exit TUI (or interrupt agent if busy; or exit bash mode) | +| **Esc** | Close active overlay / exit bash mode | +| **Ctrl+Y** | Copy full conversation to clipboard (plain text, ANSI stripped) | + +### Viewport Navigation (Scroll-Lock) | Shortcut | Action | |----------|--------| -| **Enter** | Send message / Execute command | -| **Alt+Enter** | Insert new line in message | -| **Ctrl+C** | Exit TUI / Cancel operation | -| **Ctrl+D** | Show help overlay | -| **Esc** | Close current overlay / Cancel | +| **PgUp** / **Ctrl+B** | Scroll up (disables auto-follow) | +| **PgDn** | Scroll down (re-enables auto-follow when at bottom) | +| **G** | Jump to bottom and resume auto-follow (when scroll-locked) | -### Navigation Shortcuts +### Tool Result Access | Shortcut | Action | |----------|--------| -| **↑ / ↓** | Scroll chat history | -| **PgUp / PgDn** | Page up/down in overlays | -| **Tab** | Navigate between buttons in overlays | -| **Space** | Toggle selection in approval dialogs | +| **Ctrl+V** | View the last tool result in a full overlay | +| **Ctrl+L** | Open result history β€” browse all tool results from the session | ### Command Palette | Shortcut | Action | |----------|--------| -| **/** | Open command palette | -| **↑ / ↓** | Navigate commands | -| **Enter** | Execute selected command | -| **Esc** | Close palette | +| **Ctrl+K** / **Ctrl+P** | Toggle command palette | +| **/` (slash)** | Open command palette via input (type `/` as first character) | +| **↑ / ↓** | Navigate commands in palette | +| **Tab** | Autocomplete selected command and close palette | +| **Enter** | Execute selected command immediately | +| **Esc** | Close palette without executing | + +--- + +## Smart Scroll-Lock + +The TUI implements smart scroll-lock (ADR-0048) to let you review previous output while the agent is still generating new content. + +### How It Works + +1. **Auto-follow mode** (default): The viewport scrolls down automatically as new agent output arrives. +2. **Scroll-lock mode**: Activated automatically when you scroll up (PgUp or mouse wheel up). Auto-follow is paused. +3. **New content indicator**: While scroll-locked, if the agent produces new output, a banner appears at the bottom of the viewport: + ``` + ↓ New content below β€” press G or PgDn to follow + ``` +4. **Resume auto-follow**: Press **G** (anywhere in the input) or **PgDn** (when at the bottom) to jump back to the latest content and re-enable auto-follow. + +This allows you to review earlier parts of a long response without missing what the agent is currently writing. + +--- + +## Clipboard Copy + +Press **Ctrl+Y** at any time to copy the full conversation history to your system clipboard. + +- The entire conversation buffer is copied (not just the visible viewport). +- ANSI color codes are automatically stripped β€” the clipboard receives clean plain text. +- A toast notification confirms success or reports an error (e.g., if no clipboard manager is available). + +**Tip:** This is especially useful for sharing agent output in tickets, pull requests, or code reviews. --- ## Slash Commands -Slash commands provide quick access to TUI features and agent actions. Type `/` to open the command palette. +Slash commands provide quick access to TUI features and agent actions. Type `/` to open the command palette, or type `/command` directly. ### Available Commands -#### `/help` - Show Help Information +#### `/help` β€” Show Help Information + ``` /help ``` -Displays a help overlay with: -- Available commands -- Keyboard shortcuts -- Usage tips -#### `/stop` - Stop Agent Operation +Displays the help overlay with all keyboard shortcuts and commands. + +#### `/stop` β€” Stop Agent Operation + ``` /stop ``` -Immediately stops the current agent operation. Use this if the agent is stuck or you want to cancel an action. -#### `/commit` - Create Git Commit +Immediately interrupts the current agent operation. Use this if the agent is stuck or you want to cancel an action. + +#### `/commit` β€” Create Git Commit + ``` /commit [message] ``` -Creates a git commit with all changes from the current session. + +Creates a git commit with all changes from the current session. If no message is provided, the agent generates one based on the changes. **Examples:** ``` @@ -159,286 +283,233 @@ Creates a git commit with all changes from the current session. /commit ``` -If you don't provide a message, the agent will generate one based on the changes. - -**Note:** This command requires approval before execution. +#### `/pr` β€” Create Pull Request -#### `/pr` - Create Pull Request ``` /pr [title] ``` -Creates a pull request from the current branch. -**Examples:** -``` -/pr Add dark mode support -/pr -``` +Creates a pull request from the current branch. If no title is provided, the agent generates one. -If you don't provide a title, the agent will generate one. +**Note:** Requires a configured git remote. -**Note:** This command requires approval and git remote must be configured. +#### `/settings` β€” Open Settings -#### `/settings` - Open Settings ``` /settings ``` -Opens the interactive settings overlay where you can configure: -- Auto-approval rules -- LLM provider settings -- Display preferences -#### `/context` - Show Context Information +Opens the interactive settings overlay for configuring LLM parameters, auto-approval rules, UI preferences, and more. + +#### `/context` β€” Show Context Information + ``` /context ``` -Displays detailed information about: -- Current workspace -- Conversation history -- Token usage -- Memory state -#### `/bash` - Enter Bash Mode +Displays detailed information about the current workspace, conversation history, token usage, and memory state. + +#### `/bash` β€” Enter Bash Mode + ``` /bash ``` -Switches to bash command mode for direct shell command execution. Type `exit` to return to normal mode. -#### `/exportcontext` - Export Context Snapshot +Switches to bash command mode for direct shell command execution. The `❯` prompt turns green. Type `exit` to return to normal mode, or press **Ctrl+C** / **Esc**. + +#### `/notes` β€” Browse Agent Notes + +``` +/notes +``` + +Opens the notes viewer overlay to browse scratchpad notes created by the agent during the session. + +#### `/snapshot` β€” Export Context Snapshot + ``` -/exportcontext +/snapshot ``` -Dumps the full, live conversation payload (as seen by the LLM) to a timestamped JSON file. This is useful for debugging context management, summarization, and goal compaction. -- **Output Path**: `/.forge/context/context-.json` -- **Use Case**: Inspecting the exact data sent to the LLM at any point in the conversation. +Dumps the full live conversation payload (as seen by the LLM) to a timestamped JSON file. + +- **Output path**: `/.forge/context/context-.json` +- **Use case**: Inspecting the exact data sent to the LLM for debugging context management and summarization. --- ## Overlays -Overlays are modal dialogs that appear on top of the chat interface for specific interactions. +Overlays are modal panels that appear on top of the conversation for specific interactions. Press **Esc** to close most overlays. -### Types of Overlays +### Help Overlay (`/help`) -#### 1. Help Overlay (`/help` or Ctrl+D) - -Shows comprehensive help information including: -- Command reference -- Keyboard shortcuts -- Usage tips +Shows all keyboard shortcuts and available slash commands. **Controls:** - **↑ / ↓**: Scroll content -- **Esc**: Close overlay +- **Esc**: Close -#### 2. Settings Overlay (`/settings`) +### Settings Overlay (`/settings`) -Interactive configuration interface with tabs: -- **General**: Basic settings -- **LLM**: Provider and model configuration -- **Auto-Approval**: Configure trusted operations -- **Display**: UI preferences +Interactive configuration interface organized into collapsible sections. **Controls:** -- **Tab**: Switch between tabs -- **↑ / ↓**: Navigate options -- **Space**: Toggle checkboxes -- **Enter**: Edit text fields +- **↑ / ↓**: Navigate sections and items +- **Enter**: Edit the selected item / confirm +- **Space**: Toggle boolean settings - **Esc**: Close without saving -- **Ctrl+S**: Save changes +- **Ctrl+S**: Save and apply changes -#### 3. Context Overlay (`/context`) +### Context Overlay (`/context`) -Displays detailed context information: -- Workspace path and statistics -- Conversation history summary -- Token usage metrics -- Memory state +Displays detailed context information including workspace path, token usage, conversation history length, and active context management strategy. **Controls:** - **↑ / ↓**: Scroll content -- **Esc**: Close overlay +- **Esc**: Close -#### 4. Tool Approval Overlay +### Tool Approval Overlay -Appears when the agent requests to execute a tool that requires approval. +Appears when the agent requests to execute an operation that requires explicit approval. Shows: - Tool name and description -- Parameters being passed -- Approval buttons +- All parameters being passed +- Approve / Deny buttons **Controls:** -- **Tab**: Navigate between Approve/Deny buttons -- **Enter**: Confirm selection +- **Tab**: Move between Approve / Deny buttons +- **Enter**: Confirm the selected action +- **a**: Quick-approve +- **d**: Quick-deny - **Esc**: Deny and close -- **a**: Quick approve -- **d**: Quick deny -#### 5. Diff Viewer Overlay +### Tool Result Overlay (`Ctrl+V`) -Displays code changes with syntax highlighting when the agent modifies files. +Opens the full output of the most recent tool call in a scrollable panel. Useful for reading large file contents or long command output. -Shows: -- File path being modified -- Side-by-side or unified diff view -- Syntax-highlighted code +**Controls:** +- **↑ / ↓** / **PgUp / PgDn**: Scroll content +- **Esc**: Close + +### Result History Overlay (`Ctrl+L`) + +Shows a scrollable list of all tool results from the current session. Select any entry to view it in full. + +**Controls:** +- **↑ / ↓**: Navigate results +- **Enter**: Open selected result in full overlay +- **Esc**: Close + +### Command Palette (`Ctrl+K`, `Ctrl+P`, or `/`) + +Quick-access launcher for slash commands. + +**Controls:** +- **↑ / ↓**: Navigate commands +- **Tab**: Autocomplete the selected command into the input box +- **Enter**: Execute the selected command immediately +- **Esc**: Close without executing + +### Diff Viewer Overlay + +Displayed when the agent uses `apply_diff` to modify a file. Shows the unified diff with syntax highlighting. **Controls:** - **↑ / ↓**: Scroll through diff - **PgUp / PgDn**: Page navigation -- **Esc**: Close viewer +- **Esc**: Close -#### 6. Command Execution Overlay +### Notes Viewer Overlay (`/notes`) -Shows real-time output when executing shell commands. - -Displays: -- Command being executed -- Stdout output (live) -- Stderr output (live) -- Exit code +Browsable list of scratchpad notes created during the session. **Controls:** -- **↑ / ↓**: Scroll output -- **Ctrl+C**: Terminate command -- **Esc**: Close (after completion) +- **↑ / ↓**: Navigate notes +- **Enter**: View full note content +- **Esc**: Close / go back -#### 7. Result List Overlay +--- -Displays multiple tool results in a scrollable list. +## Agent Thinking Blocks -Shows: -- Tool name -- Execution status -- Truncated results -- Expandable details +When using models with extended thinking (e.g., Claude with thinking enabled), the agent's internal reasoning process is shown as "thinking blocks" before its response. -**Controls:** -- **↑ / ↓**: Navigate results -- **Enter**: Expand/collapse result -- **Esc**: Close overlay +### Show/Hide Thinking + +The thinking state is always visible in the status bar: + +- `βΈ« Thinking On` β€” thinking blocks are displayed in the conversation +- `βΈ« Thinking Hidden` β€” thinking is happening but not shown + +Toggle this in `/settings` under the UI section, or check the current state at a glance in the bottom-right status bar. + +### Thinking Block Display + +When enabled, thinking blocks appear in the conversation with: +- Italic muted-gray styling to distinguish them from main responses +- Elapsed time indicator showing how long the agent spent reasoning --- ## Tool Approval Workflow -The TUI implements a security-first approval system for potentially dangerous operations. +The TUI implements a security-first approval system for potentially impactful operations. ### When Approval is Required -The agent will request approval for: +The agent requests approval for: - **File writes**: Creating or modifying files - **File deletions**: Removing files - **Command execution**: Running shell commands - **Git operations**: Commits, pushes, PR creation -- **Any custom tools** marked as requiring approval - -### Approval Dialog - -When approval is needed, an overlay appears showing: - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Tool Approval Required β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β”‚ -β”‚ Tool: write_file β”‚ -β”‚ β”‚ -β”‚ Parameters: β”‚ -β”‚ path: src/main.go β”‚ -β”‚ content: package main... β”‚ -β”‚ β”‚ -β”‚ Description: β”‚ -β”‚ Write content to a file, creating it β”‚ -β”‚ if it doesn't exist or overwriting if β”‚ -β”‚ it does. β”‚ -β”‚ β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ [ Approve ] [ Deny ] β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` +- **Any tool** configured to require approval ### Making a Decision **To Approve:** -- Press **Tab** to select "Approve" button -- Press **Enter** to confirm -- Or press **a** for quick approval +- Press **a** for quick approval +- Or use **Tab** to focus the Approve button, then **Enter** **To Deny:** -- Press **Tab** to select "Deny" button -- Press **Enter** to confirm -- Or press **d** for quick denial -- Or press **Esc** to cancel +- Press **d** for quick denial +- Or use **Tab** to focus the Deny button, then **Enter** +- Or press **Esc** to deny and close ### Auto-Approval Rules -You can configure auto-approval for trusted operations in Settings: - -1. Open settings with `/settings` -2. Navigate to the "Auto-Approval" tab -3. Enable rules for: - - Read-only operations (always safe) - - Writes to specific paths - - Specific commands - - Trusted tools +Configure auto-approval for trusted operations in `/settings` under the Auto-Approval section: -**Example auto-approval rules:** -- βœ“ Auto-approve all read operations -- βœ“ Auto-approve writes to `/tmp/*` -- βœ“ Auto-approve `git status`, `git diff` -- βœ— Never auto-approve deletions +- Auto-approve read-only operations +- Auto-approve writes to specific path patterns +- Auto-approve specific shell commands +- Configure per-tool rules --- ## Settings Configuration -Access settings with the `/settings` command. +Access settings with `/settings`. -### Settings Tabs +### LLM Section -#### General Tab -- **Workspace Path**: Current working directory -- **Max Iterations**: Maximum agent loop iterations -- **Enable Toast Notifications**: Show status toasts +- **Model**: The model name used for all agent calls +- **Summarization Model**: Optional separate model for context summarization +- **Base URL**: API endpoint (for OpenAI-compatible providers) +- **API Key**: Authentication key -#### LLM Tab -- **Provider**: Select LLM provider (OpenAI, Anthropic, etc.) -- **Model**: Choose specific model -- **API Key**: Configure authentication -- **Temperature**: Control response randomness -- **Max Tokens**: Set maximum response length +### Auto-Approval Section -#### Auto-Approval Tab -- **Read Operations**: Auto-approve file reads -- **Write Operations**: Configure write rules -- **Path Patterns**: Whitelist/blacklist paths -- **Command Patterns**: Trusted commands -- **Tool-Specific Rules**: Per-tool configuration +Configure which tool calls are automatically approved without prompting. -#### Display Tab -- **Theme**: Color scheme selection -- **Font Size**: Adjust text size -- **Syntax Highlighting**: Enable/disable -- **Show Line Numbers**: In code displays -- **Diff Style**: Unified vs side-by-side - -### Saving Settings +### UI Section -Settings are automatically saved when you: -1. Press **Ctrl+S** in the settings overlay -2. Click "Save" button -3. Settings are persisted to `~/.config/forge/settings.json` +- **Show Thinking**: Toggle display of extended thinking blocks in the conversation -### Resetting Settings +### Saving Settings -To reset to defaults: -1. Open settings overlay -2. Navigate to the "General" tab -3. Click "Reset to Defaults" button -4. Confirm the action +Press **Ctrl+S** inside the settings overlay to save and apply all changes immediately. LLM settings take effect on the next agent call. --- @@ -448,66 +519,59 @@ To reset to defaults: 1. **Be Specific**: Provide clear, detailed requests - ❌ "Make the code better" - - βœ“ "Refactor the authentication function to use async/await" + - βœ“ "Refactor the authentication function to extract validation logic into a helper" 2. **Provide Context**: Reference existing code or files - - βœ“ "In src/auth.js, add error handling to the login function" + - βœ“ "In `pkg/auth/login.go`, add error handling to the `Login` function" 3. **Break Down Complex Tasks**: Split large requests into steps - - βœ“ "First, create the database schema. Then, implement the models." + - βœ“ "First create the database schema. Then implement the repository layer." ### Working with Files -1. **Review Changes**: Always review diffs before approving writes -2. **Use Version Control**: Commit frequently to track changes -3. **Backup Important Files**: Before major refactoring +1. **Review Changes**: Always read the diff before approving writes +2. **Use Version Control**: Commit frequently to checkpoint progress +3. **Backup Important Files**: Before major refactoring sessions ### Managing Long Conversations -1. **Use `/context`**: Check token usage periodically -2. **Start Fresh**: When context gets too large, start a new session -3. **Summarize**: Ask the agent to summarize work done - -### Performance Tips - -1. **Close Unused Overlays**: Press Esc to dismiss overlays -2. **Limit Output**: For large command outputs, use grep or head -3. **Batch Operations**: Group related file changes together +1. **Check Token Usage**: Watch the context bar in the bottom-right β€” orange/red means you're near the limit +2. **Context Summarization**: Forge automatically summarizes older messages to free up context when needed β€” you'll see a "Optimizing context..." toast +3. **Export Context**: Use `/snapshot` to snapshot the full conversation payload for debugging +4. **Start Fresh**: For a completely new topic, restart the TUI -### Security Best Practices +### Scroll & Navigation -1. **Review Tool Calls**: Always read approval dialogs carefully -2. **Verify Paths**: Check that file paths are correct before approving -3. **Audit Commands**: Review shell commands before execution -4. **Use Auto-Approval Carefully**: Only for truly trusted operations +1. **Stay in auto-follow**: Let the viewport follow output automatically while the agent works +2. **Scroll back freely**: PgUp to review earlier output without losing auto-follow permanently β€” just press **G** to jump back +3. **Result history**: Use **Ctrl+L** to browse all tool results without scrolling through the conversation ### Keyboard Efficiency -1. **Learn Shortcuts**: Master the essential keyboard shortcuts -2. **Use Command Palette**: Type `/` for quick access to features -3. **Navigate with Keys**: Use arrow keys instead of mouse +1. **Use the command palette**: **Ctrl+K** or **Ctrl+P** for instant slash command access +2. **Ctrl+V** for quick result inspection: No need to scroll up to find the last tool output +3. **Ctrl+Y** to share output: Copy the full session to clipboard in one keystroke ### Troubleshooting **Agent Not Responding:** - Check network connection (for cloud LLMs) -- Verify API key in settings -- Press `/stop` and try again +- Verify API key in `/settings` +- Use `/stop` and try again + +**Context Limit Errors:** +- Watch the context bar β€” switch models or start a new session if at capacity +- Use `/snapshot` to inspect what's in context **Overlays Not Closing:** -- Press **Esc** multiple times -- Try **Ctrl+C** to force close -- Restart TUI if needed +- Press **Esc** (may need to press multiple times if overlays are nested) -**Settings Not Saving:** -- Check file permissions in `~/.config/forge/` -- Verify disk space -- Check error messages in logs +**Clipboard Copy Not Working:** +- Linux requires a clipboard manager (`xclip`, `xsel`, or `wl-clipboard`) +- The TUI will show a toast error if no clipboard is available -**Tool Approval Stuck:** -- Press **Esc** to deny and close -- Use `/stop` to cancel operation -- Check agent logs for errors +**Settings Not Saving:** +- Press **Ctrl+S** explicitly inside the settings overlay (changes are not auto-saved on Esc) --- @@ -515,9 +579,11 @@ To reset to defaults: - [Getting Started Guide](../getting-started/quick-start.md) - [Understanding the Agent Loop](../getting-started/understanding-agent-loop.md) -- [Slash Commands Design](../plans/slash-commands-design.md) -- [Settings Architecture](../plans/settings-architecture.md) -- [Tool Approval System](../plans/auto-approval-and-settings.md) +- [Configure LLM Providers](configure-provider.md) +- [ADR-0048: Smart Scroll-Lock](../adr/0048-tui-smart-scroll-lock.md) +- [ADR-0049: Bracketed Paste Support](../adr/0049-tui-bracketed-paste-support.md) +- [ADR-0050: Clipboard Copy](../adr/0050-tui-clipboard-copy.md) +- [ADR-0051: Visual Redesign](../adr/0051-tui-visual-redesign.md) --- @@ -525,15 +591,7 @@ To reset to defaults: Now that you know how to use the TUI, explore: -1. **[Create Custom Tools](create-custom-tool.md)** - Extend agent capabilities -2. **[Configure LLM Providers](configure-provider.md)** - Set up different AI models -3. **[Manage Memory](manage-memory.md)** - Control conversation context -4. **[Handle Errors](handle-errors.md)** - Debug and recover from issues - ---- - -**Questions or Issues?** - -- Check the [FAQ](../FAQ.md) -- Review [Troubleshooting Guide](../community/troubleshooting.md) *(coming soon)* -- Open an issue on [GitHub](https://github.com/entrhq/forge/issues) +1. **[Create Custom Tools](create-custom-tool.md)** β€” Extend agent capabilities +2. **[Configure LLM Providers](configure-provider.md)** β€” Set up different AI models +3. **[Manage Memory](manage-memory.md)** β€” Control conversation context +4. **[Handle Errors](handle-errors.md)** β€” Debug and recover from issues diff --git a/docs/product/scratch/overlay-toast-redesign.md b/docs/product/scratch/overlay-toast-redesign.md new file mode 100644 index 0000000..7d16ba2 --- /dev/null +++ b/docs/product/scratch/overlay-toast-redesign.md @@ -0,0 +1,479 @@ +# Overlay & Toast Redesign β€” Mockups + +**Context:** The main TUI chrome was redesigned (ADR-0051) to be compact and flat β€” +a single-line header bar, `─` rule separator, and a `❯` prompt glyph with no rounded +border on the input box. Overlays and toasts still use the old rounded-border style +which now clashes. + +**Goal:** Bring overlays and toasts into the same design language, and fix the existing +problem where overlays and toasts use hardcoded dimensions that break on small terminals +or get stranded in the corner on large ones. + +--- + +## Design Principles (carried from ADR-0051) + +- No rounded corners / `RoundedBorder()` β€” use straight box-drawing or flat rules +- No emoji +- Consistent color palette: salmonPink accent, mintGreen success, mutedGray secondary, brightWhite text +- Keyboard hints in `mutedGray italic`, kept short +- Labels flat-bold, not button-box padded unless selection state requires it +- **All overlays and toasts must respond to window resize events** β€” no hardcoded pixel/cell counts + +--- + +## Responsive Sizing Model + +### The Core Problem with the Current Code + +The existing overlays use hardcoded constants: +```go +const overlayWidth = 80 // help.go +const overlayHeight = 25 +const viewportWidth = 76 +const viewportHeight = 20 +``` + +On a 60-column terminal the overlay is wider than the screen and gets clipped. +On a 200-column terminal it floats as a tiny fixed box in the centre. +Viewport heights are fixed too β€” content overflows into the border on short terminals. + +### Width: Percentage-Based with Min/Max Clamps + +Every overlay computes its display width from the live terminal width at render time: + +``` +overlayWidth = clamp(terminalWidth Γ— widthFactor, minWidth, maxWidth) +``` + +| Overlay type | widthFactor | minWidth | maxWidth | +|---------------------|-------------|----------|----------| +| Approval / Diff | 90% | 60 | 140 | +| Help / Context | 80% | 56 | 100 | +| Settings | 80% | 56 | 100 | +| Notes | 80% | 56 | 100 | +| Toast (simple) | 70% | 40 | 90 | +| Toast (with detail) | 70% | 40 | 90 | + +The inner content width is always `overlayWidth - 4` (2 chars border + 2 chars padding each side). + +### Height: Dynamic from Available Space + +The total visible terminal height is known from the last `tea.WindowSizeMsg`. Overlays +must not exceed it: + +``` +maxOverlayHeight = terminalHeight - 4 // 2 line top margin, 2 line bottom margin +viewportHeight = maxOverlayHeight - overlayChrome +``` + +`overlayChrome` = the fixed rows the overlay needs around the viewport: +- 1 row: top border (with title) +- 1 row: subtitle / separator +- 1 row: blank padding +- (for approval) 1 row: buttons, 1 row: hints +- 1 row: bottom border +Typical chrome is 5–7 rows. On a 24-line terminal a diff viewer would get ~17 lines of +viewport instead of a hardcoded 10. + +If the terminal is so short that even the chrome doesn't fit (< 12 lines), the overlay +falls back to a compact one-line toast indicating the overlay can't be shown at this size. + +### Content: Wrap, Don't Clip + +All text inside overlays must be word-wrapped to `contentWidth` before rendering. +Long file paths, tool argument strings, API key values, etc. must be truncated with `…` +at the right edge, never allowed to overflow the box border. + +Diff content is already line-based and can be scrolled β€” no truncation needed there. + +### Resize Events: Overlays Must Respond + +Every overlay type must implement a resize handler that recomputes its width, height, +and internal viewport dimensions when a `tea.WindowSizeMsg` arrives. Currently most +overlays ignore resize events while open. + +Pattern for each overlay's `Update()`: +```go +case tea.WindowSizeMsg: + o.width, o.height = computeOverlayDimensions(msg.Width, msg.Height) + o.viewport.Width = o.width - 4 + o.viewport.Height = computeViewportHeight(o.height, o.chromeRows) +``` + +--- + +## Terminal Size Breakpoints + +| Terminal width | Mode | Behaviour | +|----------------|----------|---------------------------------------------------------| +| < 50 cols | Micro | Overlays suppressed; toast shown as 1-line status bar | +| 50–79 cols | Compact | Overlays use 95% width; single-column button layout | +| 80–119 cols | Normal | Standard layout as per mockups below | +| β‰₯ 120 cols | Wide | Overlays capped at maxWidth; extra space stays as margin| + +In compact mode (50–79 cols), buttons stack vertically instead of side by side: +``` + β”Œβ”€ Tool Approval Required ──────────────────┐ + β”‚ write_file Β· src/main.go β”‚ + β”‚ ───────────────────────────────────── β”‚ + β”‚ @@ -12,7 +12,9 @@ β”‚ + β”‚ - old line β”‚ + β”‚ + new line β”‚ + β”‚ ───────────────────────────────────── β”‚ + β”‚ [ βœ“ Accept ] β”‚ + β”‚ [ βœ— Reject ] β”‚ + β”‚ Ctrl+A Β· Ctrl+R Β· Tab Β· ↑↓ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +In micro mode (< 50 cols), the overlay is suppressed and a toast replaces it: +``` + ! Overlay requires β‰₯ 50 cols to display +``` + +--- + +## Overlay Container β€” Selected Design: Option C + +Straight corners (`β”Œ ┐ β”” β”˜`), **mutedGray border**, title text embedded in the top +rule in **salmonPink**. Dimmed background via `lipgloss.Place`. No nested rounded boxes. + +``` + β”Œβ”€ Title Here ──────────────────────────────────────────────┐ + β”‚ Subtitle or secondary info β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ [scrollable content area] β”‚ + β”‚ β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ [ βœ“ Accept ] [ βœ— Reject ] β”‚ + β”‚ Ctrl+A accept Β· Ctrl+R reject Β· Tab toggle Β· ↑↓ scroll β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Width and height are computed dynamically (see Responsive Sizing Model above). + +--- + +## Overlay Type Mockups + +### 1. Diff Viewer / Tool Approval + +**Normal terminal (β‰₯ 80 cols):** +``` + β”Œβ”€ Tool Approval Required ──────────────────────────────────┐ + β”‚ write_file Β· src/main.go β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ @@ -12,7 +12,9 @@ func main() { β”‚ + β”‚ ctx := context.Background() β”‚ + β”‚ - client := NewClient() β”‚ + β”‚ + client := NewClient(cfg) β”‚ + β”‚ + defer client.Close() β”‚ + β”‚ return client.Run(ctx) β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ [ βœ“ Accept ] [ βœ— Reject ] β”‚ + β”‚ Ctrl+A accept Β· Ctrl+R reject Β· Tab toggle Β· ↑↓ scroll β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Compact terminal (50–79 cols):** +``` + β”Œβ”€ Tool Approval Required ──────────────┐ + β”‚ write_file Β· src/main.go β”‚ + β”‚ ───────────────────────────────── β”‚ + β”‚ @@ -12,7 +12,9 @@ func main() { β”‚ + β”‚ - client := NewClient() β”‚ + β”‚ + client := NewClient(cfg) β”‚ + β”‚ ───────────────────────────────── β”‚ + β”‚ [ βœ“ Accept ] β”‚ + β”‚ [ βœ— Reject ] β”‚ + β”‚ Ctrl+A Β· Ctrl+R Β· Tab Β· ↑↓ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Key changes from current: +- Straight corners, mutedGray border (not salmonPink rounded border) +- Title embedded in top rule in salmonPink text +- Inner content uses plain `─` separator rules β€” no nested box-in-box +- Buttons in compact mode stack vertically +- Viewport height computed dynamically from terminal height + +--- + +### 2. Help Overlay + +**Normal terminal (β‰₯ 80 cols):** +``` + β”Œβ”€ Keyboard Shortcuts ──────────────────────────────────────┐ + β”‚ β”‚ + β”‚ Navigation β”‚ + β”‚ ────────── β”‚ + β”‚ Enter send message β”‚ + β”‚ Alt+Enter new line β”‚ + β”‚ ↑ / ↓ scroll viewport β”‚ + β”‚ PgUp / PgDn scroll by page β”‚ + β”‚ G jump to bottom β”‚ + β”‚ β”‚ + β”‚ Commands β”‚ + β”‚ ────────── β”‚ + β”‚ /help show this overlay β”‚ + β”‚ /settings open settings β”‚ + β”‚ /notes view scratchpad β”‚ + β”‚ /context view token usage β”‚ + β”‚ /bash toggle bash mode β”‚ + β”‚ β”‚ + β”‚ Esc or Enter to close β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Compact terminal (50–79 cols):** +``` + β”Œβ”€ Keyboard Shortcuts ──────────────┐ + β”‚ β”‚ + β”‚ Navigation β”‚ + β”‚ ────────── β”‚ + β”‚ Enter send message β”‚ + β”‚ Alt+Enter new line β”‚ + β”‚ ↑ / ↓ scroll β”‚ + β”‚ G jump to bottom β”‚ + β”‚ β”‚ + β”‚ Commands β”‚ + β”‚ ────────── β”‚ + β”‚ /help this overlay β”‚ + β”‚ /settings settings β”‚ + β”‚ /notes scratchpad β”‚ + β”‚ β”‚ + β”‚ Esc to close β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Notes: +- Content is a scrollable viewport β€” if terminal is short, user scrolls to see more +- No truncation of keybindings; descriptions may be shortened on narrow terminals +- Width computed as `min(terminalWidth Γ— 0.80, 100)`, min 56 cols + +--- + +### 3. Context Information Overlay + +**Normal terminal (β‰₯ 80 cols):** +``` + β”Œβ”€ Context Information ─────────────────────────────────────┐ + β”‚ β”‚ + β”‚ Token Usage β”‚ + β”‚ ctx β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ 92k / 128k 72% β”‚ + β”‚ β”‚ + β”‚ Breakdown β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ System prompt 8,241 tokens β”‚ + β”‚ Tool definitions 4,102 tokens 32 tools β”‚ + β”‚ Conversation history 79,312 tokens 48 messages β”‚ + β”‚ raw messages 61,100 tokens 44 msgs β”‚ + β”‚ summaries 18,212 tokens 4 blocks β”‚ + β”‚ β”‚ + β”‚ Cumulative β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ Input tokens 142,000 β”‚ + β”‚ Output tokens 28,500 β”‚ + β”‚ Total tokens 170,500 β”‚ + β”‚ β”‚ + β”‚ Esc to close β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Notes: +- Progress bar is the same `β–ˆβ–‘` style as the main status bar (reuses same render function) +- Numbers are right-aligned within their column β€” column widths computed from content +- Scrollable viewport if terminal is short + +--- + +### 4. Notes Overlay + +**Normal terminal (β‰₯ 80 cols):** +``` + β”Œβ”€ Scratchpad Notes (3) ────────────────────────────────────┐ + β”‚ β”‚ + β”‚ > [decision, auth] β”‚ + β”‚ Use JWT with refresh tokens for auth scaling β”‚ + β”‚ β”‚ + β”‚ [pattern, test] β”‚ + β”‚ Test suite requires DB migration before running β”‚ + β”‚ β”‚ + β”‚ [bug, api] β”‚ + β”‚ Payment service 500s when user context missing β”‚ + β”‚ β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ ↑↓ select Β· Enter view Β· Esc close β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Compact terminal (50–79 cols):** +``` + β”Œβ”€ Scratchpad Notes (3) ──────────┐ + β”‚ β”‚ + β”‚ > [decision, auth] β”‚ + β”‚ Use JWT with refresh toke… β”‚ + β”‚ β”‚ + β”‚ [pattern, test] β”‚ + β”‚ Test suite requires DB mi… β”‚ + β”‚ β”‚ + β”‚ ───────────────────────────── β”‚ + β”‚ ↑↓ Β· Enter Β· Esc β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Notes: +- Selected item shown with `>` glyph in salmonPink +- Content preview truncated to `contentWidth - 4` with `…` suffix (never clips into border) +- Tags in mutedGray, content in brightWhite +- Width from `min(terminalWidth Γ— 0.80, 100)`, min 56 cols + +--- + +### 5. Settings Overlay (abbreviated) + +**Normal terminal (β‰₯ 80 cols):** +``` + β”Œβ”€ Settings ────────────────────────────────────────────────┐ + β”‚ β”‚ + β”‚ Provider β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ API Key β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’[sk-ant-...] β”‚ + β”‚ Model claude-opus-4-5 β”‚ + β”‚ Max Tokens 8192 β”‚ + β”‚ β”‚ + β”‚ Behaviour β”‚ + β”‚ ───────────────────────────────────────────────────── β”‚ + β”‚ Auto-compact on β”‚ + β”‚ Max turns 50 β”‚ + β”‚ β”‚ + β”‚ Tab next Β· Shift+Tab prev Β· Enter edit Β· Esc close β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Editing a field (inline, not a pop-up dialog):** +``` + β”Œβ”€ Settings ────────────────────────────────────────────────┐ + β”‚ ... β”‚ + β”‚ > Model claude-opus-4-5_ β”‚ + β”‚ ... β”‚ + β”‚ Enter confirm Β· Esc cancel β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +The `>` glyph (salmonPink) marks the actively-edited field. +The trailing `_` represents the text cursor position. + +Notes: +- Field name column width fixed at `max(longest field name + 2, 18)` +- Value column gets the remaining `contentWidth - fieldColWidth` +- Long values (API keys) truncated with `…` at right edge; full value visible during edit +- Settings scrolls as a viewport if there are more sections than terminal height allows + +--- + +## Toast Mockups + +Toasts are transient overlays above the input box (~3 second lifetime). +They use `renderToastOverlay()` which overlays lines above the input. + +### Simple Toast (success/info β€” one line, no border) + +``` + βœ“ Settings saved successfully +``` + +- No border β€” flat styled line +- Icon + message on one line +- salmonPink icon for neutral/success; ProgressRed for error +- Left-padded 2 chars from left edge +- Width: message is truncated at `terminalWidth - 6` with `…` if too long + +### Error Toast (one line) + +``` + βœ— Failed to save: permission denied +``` + +- Same flat one-line style; icon and text in ProgressRed + +### Toast with Detail (two lines, thin straight box) + +When a toast has both a message and a detail string: + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ βœ“ Git commit created β”‚ + β”‚ abc1234 Β· feat: add new overlay design β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +- Straight corners, mutedGray border +- Width: `min(terminalWidth - 8, 90)`, min 40 +- Both lines truncated at `contentWidth - 2` with `…` + +### Summarization In-Progress (persistent, not a toast) + +Shown while context summarization is running β€” stays until complete: + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β—† Optimizing context [selective-summary] β”‚ + β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ 12/20 items (60%) β”‚ + β”‚ Summarizing turn 12 / tool result β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +- Straight corners, mutedGray border +- `β—†` glyph (not emoji) +- Progress bar reuses `β–ˆβ–‘` style (same render function as status bar and context overlay) +- Width: `min(terminalWidth - 8, 90)`, min 40 +- On narrow terminals the progress bar shrinks to fit: `barWidth = contentWidth - 20` + +--- + +## Responsive Rules β€” Implementation Checklist + +These are the concrete constraints every overlay and toast implementation must satisfy: + +### Width +- [ ] No hardcoded `overlayWidth = 80` constants β€” compute from `terminalWidth` +- [ ] `contentWidth = overlayWidth - 4` (border + padding) +- [ ] All text truncated at `contentWidth` with `…` before rendering +- [ ] `lipgloss.Width()` used for all width measurements, never `len()` + +### Height +- [ ] No hardcoded `viewportHeight = 20` constants +- [ ] `viewportHeight = terminalHeight - chromeRows - 4` (top/bottom margin) +- [ ] `chromeRows` is a const per overlay type (title row + separator + footer rows) +- [ ] If `viewportHeight < 3`, show compact "terminal too small" notice instead + +### Resize +- [ ] Every overlay handles `tea.WindowSizeMsg` and recomputes its dimensions +- [ ] `BaseOverlay.Update()` routes `tea.WindowSizeMsg` to resize handler +- [ ] Viewport height is updated: `o.viewport.Height = newViewportHeight` + +### Compact mode (50–79 cols) +- [ ] Approval buttons stack vertically (one per line) +- [ ] Hint text shortened: `"Ctrl+A Β· Ctrl+R Β· ↑↓"` instead of full sentence +- [ ] Column layout in context/settings collapses to label-then-value on next line + +### Micro mode (< 50 cols) +- [ ] Modal overlays suppressed +- [ ] A single-line toast shown instead: `! Terminal too narrow for overlay` + +### Toasts +- [ ] Width: `min(terminalWidth - 8, 90)`, min 40 +- [ ] Message truncated at `toastWidth - 6` with `…` +- [ ] Positioned above input: `startLine = len(baseLines) - 5 - toastHeight` + +--- + +## Open Questions (resolved) + +1. **Overlay container style**: Option C (straight corners, mutedGray border, title in rule) β€” confirmed. +2. **Toast style**: flat one-line for simple; compact straight-corner box when detail line present β€” confirmed. +3. **Notes selected item**: `>` glyph in salmonPink (replace bubble list full-line highlight). +4. **Settings edit state**: inline `>` glyph + cursor in field row (not a pop-up dialog). +5. **Responsive**: all dimensions computed dynamically from terminal size; hardcoded constants removed. diff --git a/docs/product/scratch/slash-menu-discoverability.md b/docs/product/scratch/slash-menu-discoverability.md new file mode 100644 index 0000000..5df15f6 --- /dev/null +++ b/docs/product/scratch/slash-menu-discoverability.md @@ -0,0 +1,83 @@ +# Slash Command Palette: Discoverability Redesign + +## The Problem +Currently, the `/` command palette hardcodes `maxVisible = 5`. If there are 15 available commands, 10 are hidden behind a message saying `... and more. Keep typing to filter.` This forces users to guess or memorize commands, which hurts UX and feature discoverability. + +## Goals +1. Increase the number of commands visible without overwhelming the terminal. +2. Provide a clear indication when there are more commands off-screen. +3. Allow users to seamlessly navigate through all commands using the arrow keys (scrolling the list). + +--- + +## Option A: Dynamic Height with Viewport Scrolling (Recommended) + +Instead of a hard limit of 5, the palette expands its height to fit the available commands, up to a maximum (e.g., 40% of the terminal height or max 12 items). If the list exceeds this, it becomes a scrollable viewport with a sleek scroll indicator track on the right margin. + +### Mockup + +```text +β”Œβ”€ Slash Commands ─────────────────────────────────────────────────────────┐ +β”‚ ❯ /clear Clear the current chat session and start fresh β–ˆ β”‚ +β”‚ /commit Generate a commit message and commit changes β•‘ β”‚ +β”‚ /help Show detailed help and keyboard shortcuts β•‘ β”‚ +β”‚ /history Show recent chat history β•‘ β”‚ +β”‚ /pin Pin a file to context permanently β•‘ β”‚ +β”‚ /unpin Remove a file from pinned context β•‘ β”‚ +β”‚ /pr Review branch and create a Pull Request β•‘ β”‚ +β”‚ /model Switch the active LLM model β•‘ β”‚ +β”‚ /settings Open the settings and configuration menu β•‘ β”‚ +β”‚ /notes Open the scratchpad notes workspace β–‘ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**How it works:** +* The `maxVisible` becomes dynamic: `min(len(commands), MaxAllowedHeight)`. +* Pressing `Down` at the bottom of the visible list increments a `scrollOffset`, pushing the list up like a standard viewport list. +* A minimalistic scrollbar `β–ˆ`/`β•‘`/`β–‘` on the right edge indicates relative position. +* **Pros:** Familiar modern command palette feel (like VSCode/Raycast). Fits any screen size. +* **Cons:** Requires slightly more logic to track `scrollOffset`. + +--- + +## Option B: Categorized / Grouped View + +If we plan to add many more commands in the future, a flat list might get too long. We can group commands by domain (e.g., General, Git, Context) and show categorical headers. This breaks the list into easily skimmable chunks. + +### Mockup + +```text +β”Œβ”€ Slash Commands ─────────────────────────────────────────────────────────┐ +β”‚ β”‚ +β”‚ CORE ────────────────────────────────────────────────────────────────── β”‚ +β”‚ ❯ /clear Clear the current chat session and start fresh β”‚ +β”‚ /help Show detailed help and keyboard shortcuts β”‚ +β”‚ /settings Open the settings and configuration menu β”‚ +β”‚ β”‚ +β”‚ CONTEXT ─────────────────────────────────────────────────────────────── β”‚ +β”‚ /pin Pin a file to context permanently β”‚ +β”‚ /unpin Remove a file from pinned context β”‚ +β”‚ /notes Open the scratchpad notes workspace β”‚ +β”‚ β”‚ +β”‚ GIT ─────────────────────────────────────────────────────────────────── β”‚ +β”‚ /commit Generate a commit message and commit changes β”‚ +β”‚ /pr Review branch and create a Pull Request β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**How it works:** +* Slash commands are augmented with a `Category` field in their registry. +* The UI groups and sorts them automatically. +* **Pros:** High discoverability, visually structured and extremely organized. +* **Cons:** Taller UI footprint. + +--- + +## Technical Implementation Path + +If we proceed with **Option A (Scrollable)**, the technical changes needed in `pkg/executor/tui/overlay/palette.go` are: +1. Add `scrollOffset int` to the `CommandPalette` struct. +2. Update `navigateUp()` and `navigateDown()` to adjust `scrollOffset` when `selectedIndex` moves beyond the visible bounds (similar to how we fixed the Settings overlay!). +3. Dynamically calculate `maxVisible` based on `m.height` (the terminal height) rather than hardcoding `5`. +4. Optionally render a scroll track along the right-hand padding using standard block characters (e.g., `β–ˆ` and `β”‚` or `β•‘`). diff --git a/docs/product/scratch/tui-output-formatting.md b/docs/product/scratch/tui-output-formatting.md new file mode 100644 index 0000000..474f8c2 --- /dev/null +++ b/docs/product/scratch/tui-output-formatting.md @@ -0,0 +1,28 @@ +# TUI Output Formatting & Polish + +## Problem Statement + +The Forge TUI currently struggles with rendering command outputs, tool results, and errors cleanly in the main conversation viewport. Specifically, there are three primary issues: + +1. **Terminal Rendering Breakage (ANSI & Control Chars)**: Raw output from commands often contains ANSI escape codes, carriage returns (`\r`), backspaces (`\b`), and other non-printable control characters. When written to the main chat viewport (`m.content`), these characters break `lipgloss` text measuring and wrapping logic, causing the viewport to miscalculate heights or corrupt previous lines. +2. **Command Output Spew**: Unlike other tools which show a concise 1-line summary and a 3-line preview in the chat log, `execute_command` streams its *entire* unedited output directly into the main conversation log. This completely overwhelms the chat history when running commands like `npm install` or `go test`. +3. **Verbose, Alarming Errors**: When a tool fails or an agent loop encounters an error (via `pkgtypes.EventTypeError`), the TUI currently dumps the entire raw error payload into the chat log using a loud, bright `errorStyle`. Since Forge is an autonomous agent, these are often just intermediate bumps that the agent can recover from (like a mismatched filename), not catastrophic application failures. Showing massive red error logs overwhelms the user. + +## Proposed Solution & Implementation Plan + +### 1. Robust Output Sanitization +We need a thorough sanitization function to guarantee terminal safety. +- **Action**: Enhance the existing `helpers.go:stripANSI` function. Create a new `sanitizeOutput(s string) string` function that applies the ANSI regex AND strips/replaces problematic control characters (e.g., stripping characters `< 0x20` while preserving `\n` and `\t`). +- **Application**: Ensure `sanitizeOutput` is universally applied to any arbitrary text right before it is appended to `m.content.WriteString()` in `events.go`. + +### 2. Standardize Command Tool Results +We should bring `execute_command` into the same fold as the rest of the tools, showing a neat summary in the chat log while keeping the live-action streaming isolated to the center overlay. +- **Action 1 (Stop Spewing)**: In `pkg/executor/tui/events.go` inside `handleCommandExecutionOutput`, remove the line where it appends directly to `m.content`. (The overlay handles streaming on its own just fine via `overlay/command.go`). +- **Action 2 (Re-classify Execute Command)**: In `pkg/executor/tui/result_display.go`, remove the special-case `if toolName == "execute_command" { return TierOverlayOnly }`. This will allow command completions to fall through to standard line-count classification (`TierSummaryWithPreview`). +- **Result**: `execute_command` will correctly print a neat `βœ“ Executed command (X lines)` item with a 3-line truncated preview in the chat log, hiding the rest in the viewable overlay. + +### 3. Error Softening & Truncation +Errors should look like "agent stumbling blocks" rather than fatal app crashes. +- **Action 1 (Truncation)**: Create a `truncateLines(s string, maxLines int)` helper. +- **Action 2 (Softer Styles)**: In `pkg/executor/tui/styles.go`, implement a `warningStyle` (using `mutedGray` or another subtle, non-red tone) to replace `errorStyle` for standard agent errors. +- **Action 3 (Update Handlers)**: In `events.go:handleError`, truncate the error text to max 2 lines, change the icon to something like `⚠`, and style it softly: ` ⚠ Agent encountered an issue: [truncated error]`. diff --git a/gaps.md b/gaps.md new file mode 100644 index 0000000..3cfc70d --- /dev/null +++ b/gaps.md @@ -0,0 +1,158 @@ +# TUI Reskin Implementation Gaps + +This document tracks gaps between the ADR/PRD specification and the current implementation in `pkg/executor/tui/`. + +--- + +## Gap 1 β€” Compact Header Not Implemented + +**Spec:** ADR-0051 Step 1, PRD Β§Header Design +**Files changed:** `pkg/executor/tui/view.go` (`buildHeader()`), `pkg/executor/tui/update.go` (`headerHeight`), `pkg/version/version.go` (new package) + +**Status: βœ… Resolved** + +`buildHeader()` renders the compact 2-line header bar (brand left Β· cwd centre Β· model+version right, followed by a `inputRuleStyle` separator). The ASCII art call is gone. `headerHeight = 4` in `calculateViewportHeight()` accounts for: bar (1) + separator (1) + tips (1) + blank spacer (1). + +The `version.Version` constant lives in the new `pkg/version` package (`pkg/version/version.go`). `view.go` imports it. + +--- + +## Gap 2 β€” Input Box Still Uses 4-Sided Border + +**Spec:** ADR-0051 Step 2, PRD Β§Input Design (Option B) +**Files changed:** `pkg/executor/tui/view.go` (`buildInputBox()`), `pkg/executor/tui/styles.go` + +**Status: βœ… Resolved** + +`buildInputBox()` renders a top-rule-only design: + +``` +──────────────────────────────────────────────────────────────────────────────── +❯