Skip to content

Conversation

dhth
Copy link
Owner

@dhth dhth commented Aug 17, 2025

Summary by CodeRabbit

  • New Features

    • Transient status messages that auto-hide after a few frames.
    • Debug mode (enable with HOURS_DEBUG=1) shows terminal size and message lifetime in the footer.
  • Improvements

    • Standardized, capitalized, and more consistent user-facing error and validation messages.
    • Clearer time-validation messages (future timestamps, minimum duration).
  • Refactor

    • Unified message handling into a typed, frame-limited UI messaging system.

Copy link

coderabbitai bot commented Aug 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

Refactors TUI messaging to a typed userMsg with kind and frame-limited lifetime, replaces string-based messages across handlers with errMsg/errMsgQuick, adds a debug flag to Model (sourced from HOURS_DEBUG) and threads it through InitialModel, and updates view/status bar to respect message frames.

Changes

Cohort / File(s) Summary of changes
Typed UI messaging & message helpers
internal/ui/model.go
Adds userMsgKind enum and userMsg struct (value, kind, framesLeft), userMsgDefaultFrames, and helper constructors errMsg and errMsgQuick; changes Model.message from string to userMsg; adds debug bool to Model.
Message usage and error-wrapping across handlers
internal/ui/handle.go
Replaces direct string assignments and direct err.Error() usages with errMsg(...) or errMsgQuick(...) across task creation/update, TL validation, activation/deactivation, fetch/insert/edit flows and related messages; preserves control flow.
Ephemeral message lifecycle in Update
internal/ui/update.go
Removes plain m.message = "" initialization; makes Update decrement m.message.framesLeft each tick and clear m.message.value when frames expire; converts existing message assignments to use errMsg/errMsgQuick.
View/status bar and debug footer
internal/ui/view.go
Shows status bar only when m.message.framesLeft > 0 and m.message.value != ""; when m.debug is true appends terminal size and framesLeft info to footer.
Initial model and wiring of debug flag
internal/ui/initial.go, internal/ui/ui.go
Changes InitialModel signature to accept debug bool; reads HOURS_DEBUG in UI entrypoint (os.Getenv("HOURS_DEBUG") == "1") and passes it to InitialModel; adjusts model initialization to set debug.
Changelog
CHANGELOG.md
Updates Added/Changed entries to mention filtering tasks by status in analytics and that user messages remain visible for a while; tweaks other changelog lines.

Sequence Diagram(s)

sequenceDiagram
  participant Env as Env (HOURS_DEBUG)
  participant UI as UI Entrypoint
  participant DB as *sql.DB
  participant Model as Model
  participant View as View

  Env-->>UI: Read HOURS_DEBUG
  UI->>Model: InitialModel(DB, style, debug)
  activate Model
  Model-->>UI: Initialized Model{debug}
  deactivate Model
  UI->>View: Start program with Model
Loading
sequenceDiagram
  participant User as User
  participant Update as Update loop
  participant Model as Model.message (userMsg)
  participant View as View/status bar

  User->>Update: Trigger action
  Update->>Model: set m.message = errMsg(...) / errMsgQuick(...)
  loop per frame
    Update->>Model: if framesLeft > 0 then framesLeft--
    View->>Model: read value and framesLeft
    alt framesLeft > 0 and value != ""
      View-->>User: render status message
    else
      View-->>User: empty status bar
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit taps the terminal, bright—
Frames hop down, messages take flight.
"DEBUG=1" I twitch my ear,
Footers show what devs should hear.
Errors dressed and timed just right,
Three hops, then gone into the night.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between f051033 and 0d76f38.

📒 Files selected for processing (1)
  • CHANGELOG.md (1 hunks)
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch persist-user-messages-for-a-while

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @dhth, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

The pull request enhances the user experience by making transient UI messages, such as error or informational alerts, persist for a short duration. Previously, these messages would disappear immediately, making them hard to notice. This change introduces a mechanism to display messages for a few frames, improving visibility and user feedback within the terminal UI.

Highlights

  • Persistent UI Messages: Error and informational messages displayed to the user now remain visible for a configurable number of frames, preventing immediate disappearance.
  • Structured Message Handling: The internal Model now uses a dedicated userMsg struct to manage messages, encapsulating the message content, its type (info or error), and its display duration.
  • Standardized Message Creation: New helper functions, errMsg and errMsgQuick, have been introduced to consistently create and assign these structured messages, simplifying message management across the application.
  • Improved User Feedback: By ensuring messages persist, users have more time to read and understand alerts, leading to a more intuitive and less frustrating interaction with the application.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to persist user messages for a few frames in the UI, which is a great improvement for user experience. The implementation is solid, introducing a userMsg struct and helper functions to manage message display duration.

I've added a couple of suggestions:

  • One to improve maintainability by removing a magic number.
  • Another to fix a potential UI layout issue by re-introducing message trimming for the status bar.

Overall, great work on improving the user feedback mechanism.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (3)
internal/ui/handle.go (3)

698-704: Critical: taking the address of a range variable stores the same pointer.

Using &task inside a range loop captures the loop variable's address, which is reused each iteration; all pointers end up pointing to the last element. This breaks selection, updates, and any pointer-based lookup.

Apply this diff to materialize a new variable per iteration:

-        for i, task := range msg.tasks {
-            task.UpdateListTitle()
-            task.UpdateListDesc()
-            tasks[i] = &task
-            m.taskMap[task.ID] = &task
-            m.taskIndexMap[task.ID] = i
-        }
+        for i := range msg.tasks {
+            t := msg.tasks[i] // copy to a new variable to take a stable address
+            t.UpdateListTitle()
+            t.UpdateListDesc()
+            tasks[i] = &t
+            m.taskMap[t.ID] = &t
+            m.taskIndexMap[t.ID] = i
+        }

712-716: Same pointer-of-range bug for inactive tasks.

All pointers will reference the last item.

Apply this diff:

-        for i, inactiveTask := range msg.tasks {
-            inactiveTask.UpdateListTitle()
-            inactiveTask.UpdateListDesc()
-            inactiveTasks[i] = &inactiveTask
-        }
+        for i := range msg.tasks {
+            t := msg.tasks[i]
+            t.UpdateListTitle()
+            t.UpdateListDesc()
+            inactiveTasks[i] = &t
+        }

199-199: Bug: clearing the wrong input collection (taskInputs vs tLInputs).

In editActiveTLView escape handling, this should clear time-log inputs, not task inputs.

Apply this diff:

-        m.taskInputs[entryBeginTS].SetValue("")
+        m.tLInputs[entryBeginTS].SetValue("")
♻️ Duplicate comments (2)
internal/ui/model.go (1)

174-180: Derive “quick” frames from the default to avoid magic numbers.
Make the relationship explicit so future tuning stays consistent.

 func errMsgQuick(msg string) userMsg {
   return userMsg{
     value:      msg,
     kind:       userMsgErr,
-    framesLeft: 2,
+    framesLeft: userMsgDefaultFrames - 1,
   }
 }
internal/ui/view.go (1)

35-37: Reintroduce trimming to avoid layout break with long messages.
Directly rendering long messages can overflow the status bar. Trim to terminal width.

- if m.message.framesLeft > 0 && m.message.value != "" {
-   statusBar = m.message.value
- }
+ if m.message.framesLeft > 0 && m.message.value != "" {
+   statusBar = utils.Trim(m.message.value, m.terminalWidth)
+ }
🧹 Nitpick comments (14)
internal/ui/model.go (3)

82-94: Typed, frame-limited user message model is a solid foundation. Minor nit on type.
The structure and kind enum look good. Consider shrinking framesLeft to uint8 (or using int) to avoid unsigned arithmetic footguns and to better communicate the small domain.

Apply this diff to narrow the field type:

 type userMsg struct {
   value      string
   kind       userMsgKind
-  framesLeft uint
+  framesLeft uint8
 }

129-129: Debug flag on Model is fine; consider harmonizing env handling across the app.
You now have DEBUG (for logging) and HOURS_DEBUG (for UI footer). Optional: unify semantics/naming or doc both in README for clarity.


165-172: Add a symmetric infoMsg helper (optional).
You already model info via userMsgInfo; adding a helper improves consistency with errMsg/errMsgQuick.

Example to add near these helpers:

func infoMsg(msg string) userMsg {
	return userMsg{
		value:      msg,
		kind:       userMsgInfo,
		framesLeft: userMsgDefaultFrames,
	}
}
internal/ui/ui.go (1)

23-25: Environment switch for UI debug is fine; consider broader truthiness.
Optional: accept any non-empty HOURS_DEBUG (or common truthy forms) instead of only "1", or document this strictness.

- debug := os.Getenv("HOURS_DEBUG") == "1"
+ // Alternative: treat any non-empty as enabled
+ debug := os.Getenv("HOURS_DEBUG") != ""
internal/ui/view.go (1)

306-314: Enrich debug footer (optional).
Consider including message kind for quick diagnostics; numeric enum is fine for debug.

- if m.debug {
-   footer = fmt.Sprintf("%s [term: %dx%d] [msg frames left: %d]",
-     footer,
-     m.terminalWidth,
-     m.terminalHeight,
-     m.message.framesLeft,
-   )
- }
+ if m.debug {
+   footer = fmt.Sprintf("%s [term: %dx%d] [msg frames left: %d] [msg kind: %d]",
+     footer,
+     m.terminalWidth,
+     m.terminalHeight,
+     m.message.framesLeft,
+     m.message.kind,
+   )
+ }
internal/ui/update.go (2)

39-46: TTL countdown logic is correct; small simplification possible.
Combine the two checks to avoid the second branch when framesLeft is 0.

-if m.message.framesLeft > 0 {
-  m.message.framesLeft--
-}
-
-if m.message.framesLeft == 0 {
-  m.message.value = ""
-}
+if m.message.framesLeft > 0 {
+  m.message.framesLeft--
+  if m.message.framesLeft == 0 {
+    m.message.value = ""
+  }
+}

39-46: Optional: make TTL time-based instead of event-based.
Right now TTL advances only on Update events. If you want “for a while” to mean wall-clock time, add a tea.Tick and decrement frames on ticks instead of key events.

internal/ui/handle.go (7)

27-27: Consistent use of wrappers; nit: reuse genericErrorMsg constant.

  • Line 27: Good switch to errMsg for actionable validation feedback.
  • Line 39: Prefer the existing genericErrorMsg constant over duplicating the literal.

Apply this diff:

-            m.message = errMsg("Something went wrong")
+            m.message = errMsg(genericErrorMsg)

Also applies to: 39-39


362-362: Consider making this info message shorter-lived (errMsgQuick).

"Nothing is being tracked right now" is ephemeral guidance; errMsgQuick may be a better UX fit unless you intentionally want a longer TTL.

Apply this diff:

-        m.message = errMsg("Nothing is being tracked right now")
+        m.message = errMsgQuick("Nothing is being tracked right now")

688-688: Fix spacing around colon in error message.

"Error fetching tasks: ..." reads correctly; current string has an extra space before the colon.

Apply this diff:

-        m.message = errMsg("Error fetching tasks : " + msg.err.Error())
+        m.message = errMsg("Error fetching tasks: " + msg.err.Error())

862-862: Consider longer-lived or sticky message for reload suggestion.

suggestReloadingMsg is multi-clause guidance that users may miss with a short TTL (errMsg framesLeft=3). Consider a longer-lived variant (e.g., errMsgFrames(5)) or a “sticky until next action” mode for such remedial instructions.

Also applies to: 872-872


208-211: Nit: avoid resetting the comment input inside the loop.

m.tLCommentInput.SetValue("") is called once per iteration; move it out of the loop.

Apply this diff:

-        for i := range m.tLInputs {
-            m.tLInputs[i].SetValue("")
-            m.tLCommentInput.SetValue("")
-        }
+        for i := range m.tLInputs {
+            m.tLInputs[i].SetValue("")
+        }
+        m.tLCommentInput.SetValue("")

489-490: Standardize timestamp precision for symmetry.

Start timestamps are truncated to the second (Line 489), but quick-switch uses time.Now() (Line 533) without truncation. For consistent display/comparison and to avoid off-by-subsecond surprises, consider truncating both.

Apply this diff:

-    return quickSwitchActiveIssue(m.db, task.ID, time.Now())
+    return quickSwitchActiveIssue(m.db, task.ID, time.Now().Truncate(time.Second))

Also applies to: 533-533


439-451: Optional copy tweak for deactivation message

– The “remove filter first” guardrail is clear and concise.
– The deactivation message can be smoothed out stylistically:

- m.message = errMsg("Cannot deactivate a task being tracked; stop tracking and try again.")
+ m.message = errMsg("Cannot deactivate a task that's being tracked. Stop tracking and try again.")

– Confirmed: msgCouldntSelectATask is defined in internal/ui/update.go (line 17), so this reference is safe.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d0643a9 and f051033.

📒 Files selected for processing (6)
  • internal/ui/handle.go (28 hunks)
  • internal/ui/initial.go (2 hunks)
  • internal/ui/model.go (3 hunks)
  • internal/ui/ui.go (1 hunks)
  • internal/ui/update.go (6 hunks)
  • internal/ui/view.go (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
internal/ui/initial.go (1)
internal/ui/model.go (1)
  • Model (95-130)
internal/ui/ui.go (1)
internal/ui/initial.go (1)
  • InitialModel (18-112)
internal/ui/model.go (1)
internal/ui/msgs.go (8)
  • struct{} (9-9)
  • err (64-66)
  • tsk (68-72)
  • err (48-50)
  • tlID (36-40)
  • entries (58-62)
  • tsk (74-78)
  • tsk (26-29)
internal/ui/update.go (2)
internal/ui/cmds.go (1)
  • updateTask (156-161)
internal/ui/msgs.go (1)
  • tsk (68-72)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: live-tests (ubuntu-latest)
🔇 Additional comments (29)
internal/ui/model.go (2)

76-80: Good call: central default for message lifespan.
Introducing userMsgDefaultFrames centralizes tuning of the TTL. Looks good.


124-124: No legacy m.message string assignments or comparisons found
Our regex scans returned no instances of:

  • m.message = "<literal>"
  • m.message == "<literal>" or !=
  • raw‐string uses with backticks

It appears all call sites have been updated to use the new structured type. You can consider this migration complete.

internal/ui/initial.go (2)

75-76: Plumbing the debug flag into Model looks correct.
No concerns here.


18-18: No stale InitialModel call sites remain—resolved.

I ran a repository-wide search for InitialModel( and found only:

  • internal/ui/ui.go:25 — already updated to InitialModel(db, style, debug)

No other usages (tests, tools, etc.) exist.

internal/ui/ui.go (1)

28-29: Simplified return path from p.Run is cleaner.
Nice tightening.

internal/ui/update.go (1)

205-206: Consistent use of errMsg wrapper—LGTM.
Centralizing message creation via helpers keeps behavior uniform and ties into the frame-limited model nicely.

Also applies to: 287-288, 293-294, 305-306, 336-337, 349-350

internal/ui/handle.go (23)

20-20: Capitalization tweak improves consistency.

"Time spent needs to be at least a minute" reads better and matches sentence-case style elsewhere.


53-58: Right choice: transient parsing/future-time errors use errMsgQuick.

Using errMsgQuick for parse and future-time validation keeps these ephemeral. Looks good.


75-75: LGTM on transient error handling in finish-tracking flow.

Consistent use of errMsgQuick for parse/future checks.

Also applies to: 81-81, 89-89, 94-94


133-133: LGTM on messaging for manual create/edit TL path.

Appropriate mix of errMsgQuick for parse/future checks and errMsg for generic selection failure.

Also applies to: 139-139, 145-145, 150-150, 174-174, 182-182


371-371: LGTM: generic error on missing active index.

Appropriate fallback.


413-413: LGTM: generic error on invalid selection in edit-saved flow.


460-460: LGTM: clear message for failed TL deletion selection.


468-474: LGTM: consistent "remove filter" and generic error usage in activation flow.


485-485: LGTM: generic error on missing active task selection.


514-514: LGTM: generic error on quick-switch selection failure.


538-538: LGTM: reuse of removeFilterMsg for task-creation guard.


550-556: LGTM: remove-filter guard and generic error on update selection.


608-608: LGTM: generic error on TL details selection failure.


725-725: LGTM: surfacing DB errors to the user via errMsg.


742-742: LGTM: consistent error handling for saved TL edits.


759-759: LGTM: early return on TL fetch errors with errMsg.


786-786: LGTM: consistent error surfacing for active task fetch.


816-816: LGTM: consistent error reporting on tracking toggle failure.


826-826: LGTM: genericErrorMsg on missing task map entry is appropriate.


854-854: LGTM: error handling in quick-switch path.


886-886: LGTM: scoped error message with context on TL delete failure.


902-902: LGTM: clear messages for active TL delete path and generic fallback.

Also applies to: 909-909


922-922: LGTM: quick messages for duration validation, aligned with form UX.

Also applies to: 927-927

@dhth dhth merged commit d298983 into main Aug 17, 2025
1 check was pending
@dhth dhth deleted the persist-user-messages-for-a-while branch August 17, 2025 10:02
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