Skip to content

feat(tui): add collapsible message rendering for long content#69

Merged
iohub merged 5 commits into
mainfrom
dev-0609
Jun 9, 2026
Merged

feat(tui): add collapsible message rendering for long content#69
iohub merged 5 commits into
mainfrom
dev-0609

Conversation

@iohub

@iohub iohub commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Introduce collapsible rendering for long TUI messages and centralize mode-aware logging to avoid corrupting the TUI while improving log consistency across components.

New Features:

  • Add collapsible display for long AI and user messages in the TUI, including inline expand hints and a Ctrl+P toggle for expand/collapse.
  • Provide a full-screen history view with support for collapsing long restored messages and an updated help dialog entry for the new shortcut.
  • Introduce a centralized, mode-aware logging package that configures structured logging differently for TUI and HTTP modes and exposes shared helpers for log directory and fallback writers.

Enhancements:

  • Pre-collapse long message content when formatting events and restoring sessions to avoid first-frame flash in the TUI.
  • Adjust TUI view/update flow so dialog overlays take precedence over history mode and history key handling does not swallow dialog interactions.
  • Tighten tool and LLM logging so they share the common log directory, honor debug level environment variables, and never write directly to stdout in TUI mode.
  • Replace ad-hoc fmt-based logging in dictionary, repo knowledge manager, TUI helpers, and skills loading with structured slog calls for consistent diagnostics.
  • Update history title bar hints and help dialog text to surface new shortcuts and actions while keeping the README concise by removing a redundant developer experience section.

iohub and others added 5 commits June 9, 2026 18:11
…nes)

Automatically fold messages exceeding 20 lines in the viewport, showing a
stylish expand hint with hidden line count. Press Ctrl+P to toggle.

- Add `collapsed` field to logEntry with pre-estimation at entry creation
- Apply collapse in glamour-rendered AI responses and bordered user messages
- Add `Ctrl+P` keybinding in command mode to toggle the first visible entry
- Add collapse hint styles (CollapseHintLine / CollapseHintText) in both
  dark and light themes
- Update help dialog (EN/ZH) to document the new shortcut

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ey handling

The delete confirmation dialog (triggered by `d` in history mode) was
broken in two ways:
- The View renderer returned the history view before the dialog overlay,
  so the confirmation was never visible.
- The Update router intercepted all keystrokes for history mode,
  preventing the DialogStack key handler from processing confirm/cancel.

Fix by rendering the dialog overlay ahead of history mode and skipping
the history dispatch when a dialog is active on the stack. Also add the
`d: delete` shortcut hint to the title bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create a centralized mode-aware logging package (internal/logging) that
routes slog output to ~/.codeactor/logs/app-{date}.log in TUI mode
instead of stdout/stderr, which interferes with the bubbletea TUI display.

Key changes:
- New internal/logging/logger.go: mode-aware Init() with TUI (file-only)
  and HTTP (stderr+file) modes, with io.Discard safety net in init()
- main.go: replace os.Stdout handler with io.Discard in init(), call
  logging.Init() in runTUI/runHTTP
- internal/agents/tool_logger.go: use logging.GetFallbackWriter() instead
  of os.Stdout as fallback to prevent terminal pollution in TUI mode
- internal/llm/llm.go: use logging.GetLogDir() for path consistency
- Migrate all fmt.Printf/fmt.Fprintf(os.Stderr) to slog calls across
  dict, repo-knowledge, skills, and tui packages
@sourcery-ai

sourcery-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements collapsible rendering for long TUI messages (AI and user), introduces a centralized, mode-aware logging subsystem to avoid stdout corruption in TUI mode while unifying log paths, and replaces ad-hoc printf logging with structured slog usage across components.

Sequence diagram for collapsible TUI message rendering and toggle

sequenceDiagram
    actor User
    participant TUIModel as model
    participant Viewport
    participant LogEntry as logEntry
    participant GlamourRenderer as glamourRenderer

    User->>TUIModel: key ctrl+p
    TUIModel->>TUIModel: Update(msg)
    TUIModel->>TUIModel: toggleCollapseAtViewport()
    TUIModel->>LogEntry: collapsed = !collapsed
    TUIModel->>LogEntry: clearRenderCache()
    TUIModel->>Viewport: rebuildViewportPreservingScroll()

    User->>TUIModel: next frame render
    TUIModel->>TUIModel: renderSingleEntry(entry, width)
    alt ai_response
        TUIModel->>GlamourRenderer: Render(entry.content)
        GlamourRenderer-->>TUIModel: rendered
        TUIModel->>TUIModel: collapseContent(rendered, entry, collapseMaxLines)
        TUIModel->>LogEntry: setCachedRender(collapsed, width)
    else user_message
        TUIModel->>TUIModel: renderUserMessageBoxCollapsible(content, width, entry)
        TUIModel->>LogEntry: setCachedRender(rendered, width)
    end
Loading

File-Level Changes

Change Details Files
Add collapsible rendering and keyboard toggle for long AI and user messages in the TUI, including pre-collapse logic and history/session support.
  • Wrap glamour-rendered AI responses with collapseContent before caching and displaying, appending an expand/collapse hint line when truncated.
  • Introduce renderUserMessageBoxCollapsible to render user messages with in-box collapse hints, using a shared collapseMaxLines threshold and new collapse hint styles.
  • Add a collapsed flag to logEntry, pre-populating it for long user messages and restored history entries to avoid first-frame flashes.
  • Implement collapseContent and renderCollapseHint helpers plus toggleCollapseAtViewport, wired to a new ctrl+p keybinding and documented in the help dialog.
  • Adjust TUI view/update flow so dialog overlays take precedence over history mode and allow collapse toggling outside dialogs.
internal/tui/tui_render.go
internal/tui/tui_history.go
internal/tui/tui_model.go
internal/tui/tui_tasks.go
internal/tui/components/help_dialog.go
internal/tui/tui_view.go
internal/tui/tui_update.go
internal/tui/common/styles.go
internal/tui/tui_helpers.go
Introduce a centralized, mode-aware logging package and migrate tool, LLM, and app startup logging to use it, ensuring TUI mode never writes to stdout and unifying log directory handling.
  • Add internal/logging package with Init/Close, mode selection (TUI/HTTP), shared log directory resolution, synchronized writers, fallback handlers, and GetFallbackWriter for safe degradation.
  • Change InitToolLogger to use logging.GetLogDir and logging.GetFallbackWriter, avoiding os.Stdout and gracefully degrading when the log directory or file cannot be created.
  • Refactor initLLMLogger to use logging.GetLogDir, always create a date-stamped log file, and vary slog level based on LLM_DEBUG_LOG instead of discarding logs when not debugging.
  • Update main.go TUI and HTTP entrypoints to initialize logging with the appropriate mode, silence all slog output during init(), and close logging on exit.
  • Expose GetLogDir and GetFallbackWriter from logging for reuse across components like tool_logger and llm.
internal/logging/logger.go
internal/agents/tool_logger.go
internal/llm/llm.go
main.go
Replace stdout/stderr printf-based diagnostics in various subsystems (dict manager, repo knowledge manager, TUI helpers, skills loader, TUI event consumer, history UI) with structured slog logging consistent with the new logging system.
  • Convert dictionary manager hot-reload messages and file watch errors to slog.Warn/Info/Error with component and contextual fields.
  • Change repo knowledge manager cache/search diagnostics to slog.Info/Warn with structured metadata instead of fmt.Printf.
  • Update TUI helpers, skills loader, and TUI event consumer to log via slog.Error/Warn rather than printing to stdout/stderr.
  • Adjust history view title bar hint text to include the delete shortcut in the right-hand help area.
internal/dict/manager.go
internal/agents/repo_knowledge_manager.go
internal/tui/tui_helpers.go
internal/skills/skills.go
internal/tui/tui_model.go
internal/tui/tui_history.go
Tweak documentation and TUI history/help UX to align with the new features and keybindings.
  • Remove the "Developer Experience" marketing section from both English and Chinese READMEs to simplify top-level docs.
  • Update help dialog content (EN/ZH) to describe the new ctrl+p expand/collapse shortcut under a dedicated Messages section.
  • Update the history view title bar right-side hint to include the delete key binding label.
README.md
README_zh.md
internal/tui/components/help_dialog.go
internal/tui/tui_history.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@iohub iohub merged commit 3660306 into main Jun 9, 2026
1 check passed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In restoreSession you pre-collapse entries with eventType == "ai_response" || eventType == "user_input", but elsewhere the code consistently uses "user_message" (e.g., submitTaskWithContent, submitFollowUp, toggleCollapseAtViewport), so restored user messages likely won’t collapse as intended; consider aligning the event type checks.
  • You added CollapseHintLine/CollapseHintText styles in common/styles.go but the collapse hint rendering currently uses the global collapseHintLineStyle/collapseHintTextStyle in tui_model.go; it would be cleaner to consolidate on one style source (preferably the shared Styles struct) or remove the unused fields.
  • The comment on logEntry.collapsed says "folded (>15 lines)" while collapseMaxLines is set to 20 and used throughout; updating the comment or making the threshold configurable would keep this consistent and avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `restoreSession` you pre-collapse entries with `eventType == "ai_response" || eventType == "user_input"`, but elsewhere the code consistently uses `"user_message"` (e.g., `submitTaskWithContent`, `submitFollowUp`, `toggleCollapseAtViewport`), so restored user messages likely won’t collapse as intended; consider aligning the event type checks.
- You added `CollapseHintLine`/`CollapseHintText` styles in `common/styles.go` but the collapse hint rendering currently uses the global `collapseHintLineStyle`/`collapseHintTextStyle` in `tui_model.go`; it would be cleaner to consolidate on one style source (preferably the shared `Styles` struct) or remove the unused fields.
- The comment on `logEntry.collapsed` says "folded (>15 lines)" while `collapseMaxLines` is set to 20 and used throughout; updating the comment or making the threshold configurable would keep this consistent and avoid confusion.

## Individual Comments

### Comment 1
<location path="internal/tui/tui_history.go" line_range="376-377" />
<code_context>
 			entry.content = msg.Content
 		}

+		// Pre-collapse long ai_response and user_input entries
+		if entry.eventType == "ai_response" || entry.eventType == "user_input" {
+			if strings.Count(entry.content, "\n") >= collapseMaxLines {
+				entry.collapsed = true
</code_context>
<issue_to_address>
**issue (bug_risk):** Inconsistent eventType string for user messages may break collapse behavior in history view

This path uses "user_input" while other user messages use "user_message" (e.g. in submitTaskWithContent/submitFollowUp and toggleCollapseAtViewport). As a result, restored user messages may not be recognized as collapsible or rendered consistently. Consider switching to "user_message" here or handling both values to keep behavior aligned between live and restored sessions.
</issue_to_address>

### Comment 2
<location path="internal/tui/tui_model.go" line_range="209-210" />
<code_context>
 	resultBrief      string // brief result description (e.g., "120 lines", "modified")
 	diffText         string // unified diff content for file edit results
 	renderedCache    map[int]string // width-keyed cache: key=width, value=rendered content
+	collapsed        bool           // true if content is currently folded (>15 lines)

 	compactData *CompactData
</code_context>
<issue_to_address>
**nitpick (typo):** Collapsed field comment is out of sync with collapseMaxLines constant

This still hard-codes ">15 lines" while collapseMaxLines is 20. Please update the comment to reference collapseMaxLines (or remove the specific number) so it stays accurate if the threshold changes.

```suggestion
	renderedCache    map[int]string // width-keyed cache: key=width, value=rendered content
	collapsed        bool           // true if content is currently folded (exceeds collapseMaxLines)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +376 to +377
// Pre-collapse long ai_response and user_input entries
if entry.eventType == "ai_response" || entry.eventType == "user_input" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Inconsistent eventType string for user messages may break collapse behavior in history view

This path uses "user_input" while other user messages use "user_message" (e.g. in submitTaskWithContent/submitFollowUp and toggleCollapseAtViewport). As a result, restored user messages may not be recognized as collapsible or rendered consistently. Consider switching to "user_message" here or handling both values to keep behavior aligned between live and restored sessions.

Comment thread internal/tui/tui_model.go
Comment on lines 209 to +210
renderedCache map[int]string // width-keyed cache: key=width, value=rendered content
collapsed bool // true if content is currently folded (>15 lines)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Collapsed field comment is out of sync with collapseMaxLines constant

This still hard-codes ">15 lines" while collapseMaxLines is 20. Please update the comment to reference collapseMaxLines (or remove the specific number) so it stays accurate if the threshold changes.

Suggested change
renderedCache map[int]string // width-keyed cache: key=width, value=rendered content
collapsed bool // true if content is currently folded (>15 lines)
renderedCache map[int]string // width-keyed cache: key=width, value=rendered content
collapsed bool // true if content is currently folded (exceeds collapseMaxLines)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant