Conversation
…essage log Replace the form-based submit-and-wait TUI with a single-line prompt model that streams task events in real time. Key changes: - Add message log area with styled event types (tool calls, AI responses, errors) rendered inline during task execution - Run tasks in background goroutines, piping MessageEvents to the TUI via a channel-based consumer - Move infrastructure initialization (config, client, assistant) out of startTUI so the TUI receives pre-built dependencies - Remove button-based navigation in favor of keyboard shortcuts - Simplify redundant nil-check init pattern in assistant - Drop unused imports, dead code, and redundant comments
…le viewport Replace the fixed-height message log with a scrollable viewport and use glamour to render AI responses as formatted markdown. Viewport dimensions are recalculated on terminal resize, and content is rebuilt from log entries on each update. Non-AI-response entries fall back to plain text formatting.
Detect terminal background color before entering raw mode and pass an explicit dark/light style to glamour instead of relying on WithAutoStyle(), which queried the terminal and leaked escape sequences into the text input.
Reviewer's GuideRefactors the TUI from a single-shot form submission into a streaming, log-centric Bubble Tea application integrated directly with the CodingAssistant and TaskManager, adds markdown rendering for AI responses, and simplifies main.go’s TUI mode wiring and i18n strings while updating dependencies for the new UI stack. Sequence diagram for streaming task execution in TUI modesequenceDiagram
actor User
participant Main as main
participant TUI as model
participant TM as http_TaskManager
participant CA as assistant_CodingAssistant
participant DM as assistant_DataManager
participant Disp as messaging_MessageDispatcher
participant Cons as tuiEventConsumer
User->>Main: start with mode tui
Main->>CA: LoadConfig and NewClient and NewCodingAssistant
Main->>TM: NewTaskManager
Main->>DM: NewDataManager
Main->>TUI: startTUI(taskFilePath, CA, TM, DM)
TUI->>TUI: initialModel(preloadedTaskContent, CA, TM, DM, useDarkStyle)
TUI->>TUI: Init() listenForEvents(eventCh)
User->>TUI: type task and press enter
TUI->>TUI: submitTask()
TUI->>TUI: add status logEntry
TUI->>TUI: rebuildViewportContent()
TUI->>TUI: executeTaskCmd(taskDesc, projectDir, CA, TM, DM, eventCh)
activate TUI
TUI-->>TUI: returns tea.Cmd (background goroutine)
deactivate TUI
par background task execution
TUI->>TM: AddTask(task)
TUI->>Disp: NewMessageDispatcher
TUI->>Cons: create tuiEventConsumer with ch
TUI->>Disp: RegisterConsumer(Cons)
TUI->>CA: IntegrateMessaging(Disp)
TUI->>CA: ProcessCodingTaskWithCallback(request)
loop task runs
CA-->>Disp: Publish MessageEvent
Disp-->>Cons: Deliver MessageEvent
Cons-->>TUI: send event on eventCh
end
CA-->>TUI: result or error
TUI->>DM: SaveTaskMemory(taskID, task.Memory)
alt success
TUI->>TM: SetTaskResult(taskID, result)
else failure
TUI->>TM: SetTaskError(taskID, err)
end
TUI-->>TUI: return taskCompleteMsg
and UI event loop
loop for each MessageEvent
TUI->>TUI: listenForEvents(eventCh)
TUI-->>TUI: taskEventMsg
TUI->>TUI: formatEventAsEntry()
TUI->>TUI: append logEntry
TUI->>TUI: rebuildViewportContent()
end
TUI-->>TUI: taskCompleteMsg
TUI->>TUI: handle taskCompleteMsg
TUI->>TUI: taskRunning false and update errMsg and logEntries
TUI->>TUI: rebuildViewportContent()
end
User-->>TUI: scroll viewport and view streaming log
Updated class diagram for TUI model and related typesclassDiagram
class model {
// External dependencies
+assistant_CodingAssistant assistant
+http_TaskManager taskManager
+assistant_DataManager dataManager
// Input
+textinput_Model input
// Message log
+[]logEntry logEntries
+viewport_Model viewport
+glamour_TermRenderer glamourRenderer
+bool useDarkStyle
// Task execution state
+bool taskRunning
+chan messaging_MessageEvent eventCh
// Standard state
+int termWidth
+int termHeight
+bool quitting
+string errMsg
+string infoMsg
+Language currentLang
+string projectDir
// History modal state
+bool showHistoryModal
+[]assistant_TaskHistoryItem historyItems
+[]assistant_TaskHistoryItem filteredItems
+int historyIndex
+textinput_Model historySearch
+Init() tea_Cmd
+Update(msg tea_Msg) tea_Model
+View() string
+toggleLanguage()
+openHistoryModal()
+applyHistorySelection()
+filterHistoryList()
+renderWelcomePanel() string
+resizeViewport()
+rebuildViewportContent()
+submitTask() tea_Cmd
+computeFieldWidth() int
+renderHistoryModal() string
}
class logEntry {
+time_Time timestamp
+string eventType
+string from
+string content
+string toolName
}
class taskEventMsg {
+messaging_MessageEvent* event
}
class taskCompleteMsg {
+string taskID
+string result
+error err
}
class tuiEventConsumer {
+chan messaging_MessageEvent ch
+Consume(event messaging_MessageEvent) error
}
class assistant_CodingAssistant {
+ProcessCodingTaskWithCallback(req TaskRequest) (string, error)
+ProcessConversation(req TaskRequest) (string, error)
+Init(llm any, projectDir string)
+IntegrateMessaging(dispatcher messaging_MessageDispatcher)
}
class http_TaskManager {
+AddTask(task Task)
+SetTaskResult(id string, result string)
+SetTaskError(id string, err string)
+GetTask(id string) (Task, bool)
}
class assistant_DataManager {
+ListTaskHistory(limit int) ([]assistant_TaskHistoryItem, error)
+SaveTaskMemory(taskID string, memory memory_ConversationMemory) error
}
class messaging_MessageEvent {
+time_Time Timestamp
+string Type
+string From
+any Content
}
class messaging_MessageDispatcher {
+RegisterConsumer(consumer messaging_Consumer)
+Shutdown()
}
class tea_Cmd
class tea_Msg
class tea_Model
class textinput_Model
class viewport_Model {
+int Width
+int Height
+SetContent(content string)
+GotoBottom()
+Update(msg tea_Msg) (viewport_Model, tea_Cmd)
+View() string
}
class glamour_TermRenderer {
+Render(markdown string) (string, error)
}
class Language
model --> assistant_CodingAssistant : uses
model --> http_TaskManager : uses
model --> assistant_DataManager : uses
model --> textinput_Model : embeds
model --> viewport_Model : embeds
model --> glamour_TermRenderer : embeds
model --> logEntry : contains
model --> taskEventMsg : handles
model --> taskCompleteMsg : handles
tuiEventConsumer --> messaging_MessageEvent : consumes
tuiEventConsumer --> model : sends via eventCh
assistant_CodingAssistant --> messaging_MessageDispatcher : integrates
class http_Task {
+string ID
+string Status
+string ProjectDir
+time_Time CreatedAt
+time_Time UpdatedAt
+memory_ConversationMemory Memory
+context_Context Context
+context_CancelFunc CancelFunc
}
class memory_ConversationMemory
class context_Context
class context_CancelFunc
http_TaskManager --> http_Task : manages
class TaskRequest {
+context_Context ctx
+string taskID
+string projectDir
+string taskDesc
+string userMessage
+memory_ConversationMemory memory
+WithProjectDir(projectDir string) TaskRequest
+WithTaskDesc(taskDesc string) TaskRequest
+WithMemory(memory memory_ConversationMemory) TaskRequest
+WithMessagePublisher(publisher assistant_MessagePublisher) TaskRequest
}
assistant_CodingAssistant --> TaskRequest : processes
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 5 issues, and left some high level feedback:
- The
submitTaskflow callslistenForEvents(m.eventCh)even though an event listener is already started inInit, which can lead to multiple goroutines consuming from the same channel and non-deterministic event ordering; consider centralizing the event listener setup so it is started only once. - In
formatLogEntry, truncatingcontentwithcontent[:contentWidth-3]can cut through multi-byte runes and break alignment; consider truncating by runes or using a width-aware helper (e.g., based onrunewidth/lipgloss) to avoid splitting characters. - The i18n strings (
InfoMessage,TaskDescPlaceholder) still reference Tab/Shift+Tab navigation and non-empty placeholders, but the new TUI interaction model only uses a single input and Enter/Ctrl shortcuts; aligning these texts with the new UX would avoid confusing users.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `submitTask` flow calls `listenForEvents(m.eventCh)` even though an event listener is already started in `Init`, which can lead to multiple goroutines consuming from the same channel and non-deterministic event ordering; consider centralizing the event listener setup so it is started only once.
- In `formatLogEntry`, truncating `content` with `content[:contentWidth-3]` can cut through multi-byte runes and break alignment; consider truncating by runes or using a width-aware helper (e.g., based on `runewidth`/lipgloss) to avoid splitting characters.
- The i18n strings (`InfoMessage`, `TaskDescPlaceholder`) still reference Tab/Shift+Tab navigation and non-empty placeholders, but the new TUI interaction model only uses a single input and Enter/Ctrl shortcuts; aligning these texts with the new UX would avoid confusing users.
## Individual Comments
### Comment 1
<location path="tui.go" line_range="218-219" />
<code_context>
m.historySearch.Placeholder = langManager.GetText("HistorySearchHint")
}
func (m *model) openHistoryModal() {
- // load history
dm, err := assistant.NewDataManager()
if err == nil {
items, err2 := dm.ListTaskHistory(50)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Reuse the injected DataManager instead of constructing a new one in the history modal.
`model` already has a `dataManager *assistant.DataManager` from `initialModel`, but `openHistoryModal` always calls `assistant.NewDataManager()`. This may create multiple managers with inconsistent config or lifetime. Use `m.dataManager` instead, only creating a new one if it is nil.
</issue_to_address>
### Comment 2
<location path="tui.go" line_range="644-645" />
<code_context>
+ if contentWidth < 20 {
+ contentWidth = 20
+ }
+ if lipgloss.Width(content) > contentWidth {
+ content = content[:contentWidth-3] + "..."
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Truncate log content using runes rather than byte slices to avoid cutting multibyte characters.
Here you’re slicing `content` by bytes (`content[:contentWidth-3]`), so UTF-8 text can be cut in the middle of a multibyte rune, leading to invalid or garbled characters in the UI. Convert to `[]rune` (or use a rune-aware truncation helper) before truncating to ensure safe handling of non-ASCII content.
</issue_to_address>
### Comment 3
<location path="tui.go" line_range="539-541" />
<code_context>
+ b.WriteString("\n")
+ }
+
+ m.viewport.SetContent(b.String())
+ m.viewport.GotoBottom()
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Only auto-scroll the viewport to the bottom when the user is already at (or near) the bottom.
This will force the view to jump to the latest entry even when the user has intentionally scrolled up. Track whether the viewport was at (or near) the bottom before updating, and only call `GotoBottom()` in that case.
```suggestion
// Remember whether the viewport was at (or near) the bottom before updating.
// This avoids forcing a scroll when the user has intentionally scrolled up.
wasAtOrNearBottom := m.viewport.AtBottom() ||
m.viewport.YOffset >= m.viewport.ContentHeight-m.viewport.Height-3
m.viewport.SetContent(b.String())
if wasAtOrNearBottom {
m.viewport.GotoBottom()
}
}
```
</issue_to_address>
### Comment 4
<location path="README.md" line_range="21-22" />
<code_context>
+## Screenshots
+
+<p align="center">
+ <img src="docs/sceenshot-1.png" alt="CodeActor TUI Screenshot 1" width="49%">
+ <img src="docs/sceenshot-2.png" alt="CodeActor TUI Screenshot 2" width="49%">
+</p>
+
</code_context>
<issue_to_address>
**issue (typo):** Typo in image filenames: "sceenshot" should be "screenshot".
Both images reference `docs/sceenshot-1.png` and `docs/sceenshot-2.png`. If these are typos, please rename the files and update the `src` paths to `screenshot-1.png` / `screenshot-2.png` to fix the spelling and avoid broken image links.
Suggested implementation:
```
## Screenshots
<p align="center">
<img src="docs/screenshot-1.png" alt="CodeActor TUI Screenshot 1" width="49%">
<img src="docs/screenshot-2.png" alt="CodeActor TUI Screenshot 2" width="49%">
</p>
```
To fully implement the suggestion, you should also:
1. Rename the actual image files in the `docs` directory from `sceenshot-1.png` to `screenshot-1.png` and from `sceenshot-2.png` to `screenshot-2.png`.
2. Update any other references in the repo (if any) that still use the old misspelled filenames.
</issue_to_address>
### Comment 5
<location path="README_zh.md" line_range="21-22" />
<code_context>
+## 效果截图
+
+<p align="center">
+ <img src="docs/sceenshot-1.png" alt="CodeActor TUI 截图 1" width="49%">
+ <img src="docs/sceenshot-2.png" alt="CodeActor TUI 截图 2" width="49%">
+</p>
+
</code_context>
<issue_to_address>
**issue (typo):** Typo in image filenames: "sceenshot" should be "screenshot".
Please update the `src` paths (and underlying filenames, if needed) so they consistently use `screenshot-1.png` and `screenshot-2.png` to avoid broken images.
```suggestion
<img src="docs/screenshot-1.png" alt="CodeActor TUI 截图 1" width="49%">
<img src="docs/screenshot-2.png" alt="CodeActor TUI 截图 2" width="49%">
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| func (m *model) openHistoryModal() { | ||
| // load history | ||
| dm, err := assistant.NewDataManager() |
There was a problem hiding this comment.
suggestion (bug_risk): Reuse the injected DataManager instead of constructing a new one in the history modal.
model already has a dataManager *assistant.DataManager from initialModel, but openHistoryModal always calls assistant.NewDataManager(). This may create multiple managers with inconsistent config or lifetime. Use m.dataManager instead, only creating a new one if it is nil.
| if lipgloss.Width(content) > contentWidth { | ||
| content = content[:contentWidth-3] + "..." |
There was a problem hiding this comment.
issue (bug_risk): Truncate log content using runes rather than byte slices to avoid cutting multibyte characters.
Here you’re slicing content by bytes (content[:contentWidth-3]), so UTF-8 text can be cut in the middle of a multibyte rune, leading to invalid or garbled characters in the UI. Convert to []rune (or use a rune-aware truncation helper) before truncating to ensure safe handling of non-ASCII content.
| m.viewport.SetContent(b.String()) | ||
| m.viewport.GotoBottom() | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Only auto-scroll the viewport to the bottom when the user is already at (or near) the bottom.
This will force the view to jump to the latest entry even when the user has intentionally scrolled up. Track whether the viewport was at (or near) the bottom before updating, and only call GotoBottom() in that case.
| m.viewport.SetContent(b.String()) | |
| m.viewport.GotoBottom() | |
| } | |
| // Remember whether the viewport was at (or near) the bottom before updating. | |
| // This avoids forcing a scroll when the user has intentionally scrolled up. | |
| wasAtOrNearBottom := m.viewport.AtBottom() || | |
| m.viewport.YOffset >= m.viewport.ContentHeight-m.viewport.Height-3 | |
| m.viewport.SetContent(b.String()) | |
| if wasAtOrNearBottom { | |
| m.viewport.GotoBottom() | |
| } | |
| } |
| <img src="docs/sceenshot-1.png" alt="CodeActor TUI Screenshot 1" width="49%"> | ||
| <img src="docs/sceenshot-2.png" alt="CodeActor TUI Screenshot 2" width="49%"> |
There was a problem hiding this comment.
issue (typo): Typo in image filenames: "sceenshot" should be "screenshot".
Both images reference docs/sceenshot-1.png and docs/sceenshot-2.png. If these are typos, please rename the files and update the src paths to screenshot-1.png / screenshot-2.png to fix the spelling and avoid broken image links.
Suggested implementation:
## Screenshots
<p align="center">
<img src="docs/screenshot-1.png" alt="CodeActor TUI Screenshot 1" width="49%">
<img src="docs/screenshot-2.png" alt="CodeActor TUI Screenshot 2" width="49%">
</p>
To fully implement the suggestion, you should also:
- Rename the actual image files in the
docsdirectory fromsceenshot-1.pngtoscreenshot-1.pngand fromsceenshot-2.pngtoscreenshot-2.png. - Update any other references in the repo (if any) that still use the old misspelled filenames.
| <img src="docs/sceenshot-1.png" alt="CodeActor TUI 截图 1" width="49%"> | ||
| <img src="docs/sceenshot-2.png" alt="CodeActor TUI 截图 2" width="49%"> |
There was a problem hiding this comment.
issue (typo): Typo in image filenames: "sceenshot" should be "screenshot".
Please update the src paths (and underlying filenames, if needed) so they consistently use screenshot-1.png and screenshot-2.png to avoid broken images.
| <img src="docs/sceenshot-1.png" alt="CodeActor TUI 截图 1" width="49%"> | |
| <img src="docs/sceenshot-2.png" alt="CodeActor TUI 截图 2" width="49%"> | |
| <img src="docs/screenshot-1.png" alt="CodeActor TUI 截图 1" width="49%"> | |
| <img src="docs/screenshot-2.png" alt="CodeActor TUI 截图 2" width="49%"> |
Summary by Sourcery
Refactor the TUI into an interactive, streaming task runner with a scrollable log and integrated assistant infrastructure, while simplifying assistant initialization and updating dependencies for the new UI capabilities.
New Features:
Enhancements:
Build:
Documentation: