-
-
Notifications
You must be signed in to change notification settings - Fork 104
feat: Ability to view all logs with prefix when processes table header is selected #437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Implemented RestartAllProcesses method in the API and ProjectRunner. - Added corresponding route for restarting all processes. - Created client method to call the restart all processes API. - Integrated restart all processes action in TUI with confirmation dialog. - Updated shortcuts and help text for the new action.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds the ability to view consolidated logs from all processes simultaneously by selecting the process table header row, similar to docker-compose log output. It also introduces a "Restart All Processes" feature with both TUI and API support.
Changes:
- All-logs mode: Selecting the process table header (row 0) now displays logs from all processes with colored process name prefixes
- Restart All functionality: New API endpoint and TUI action to restart all processes in proper shutdown/startup order
- UI improvements: Process-specific shortcuts are hidden when in all-logs mode to prevent confusion
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tui/view.go | Adds all-logs mode state tracking and conditional help footer display |
| src/tui/proc-table.go | Implements header row selection handling and restart-all UI dialog |
| src/tui/log-viewer.go | Adds WriteStringWithProcess method for prefixed log output |
| src/tui/log-operations.go | Implements follow/unfollow/truncate operations for all-logs mode |
| src/tui/all-logs-observer.go | New observer implementation for multiplexing logs with colored prefixes |
| src/tui/actions.go | Defines ActionProcessRestartAll action and shortcut mapping |
| src/app/project_runner.go | Implements RestartAllProcesses with ordered shutdown and restart logic |
| src/app/project_interface.go | Adds RestartAllProcesses to IProject interface |
| src/api/routes.go | Adds POST /processes/restart endpoint |
| src/api/pc_api.go | Implements RestartAllProcesses API handler |
| src/client/restart.go | Adds client-side restartAllProcesses method |
| src/client/client.go | Exposes RestartAllProcesses in PcClient interface |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }) | ||
| pv.shortcuts.setAction(ActionClearLog, func() { | ||
| pv.logsText.Clear() | ||
| pv.truncateLog() | ||
| if pv.allLogsMode { | ||
| pv.truncateAllLogs() | ||
| } else { | ||
| pv.truncateLog() | ||
| } | ||
| }) | ||
| pv.shortcuts.setAction(ActionMarkLog, func() { | ||
| pv.logsText.AddMark() |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Process-specific actions like ActionProcessScale, ActionProcessInfo, ActionProcessStart, ActionProcessStop, and ActionProcessRestart should be guarded to prevent execution when in all-logs mode (when row 0 is selected). Currently, these actions can still be triggered via shortcuts when the header row is selected, but getSelectedProcName will return an empty string, which could cause errors or unexpected behavior. Consider adding guards in the action handlers to check if allLogsMode is true and either no-op or show an appropriate message.
| }) | ||
| pv.shortcuts.setAction(ActionClearLog, func() { | ||
| pv.logsText.Clear() | ||
| pv.truncateLog() | ||
| if pv.allLogsMode { | ||
| pv.truncateAllLogs() | ||
| } else { | ||
| pv.truncateLog() | ||
| } | ||
| }) | ||
| pv.shortcuts.setAction(ActionMarkLog, func() { | ||
| pv.logsText.AddMark() |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log title updates in ActionLogFindNext and ActionLogFindPrev handlers should use the same pattern as getLogTitle in log-operations.go to properly handle all-logs mode. These handlers use pv.getSelectedProcName() which returns empty when row 0 is selected, but getLogTitle already handles this case by checking allLogsMode. The current implementation will work, but it's inconsistent with how the title is set elsewhere when entering all-logs mode.
| p.runProcMutex.Unlock() | ||
|
|
||
| var nameOrder []string | ||
| for _, v := range shutdownOrder { | ||
| nameOrder = append(nameOrder, v.getName()) | ||
| } | ||
| log.Debug().Msgf("Stopping %d processes for restart. Order: %q", len(shutdownOrder), nameOrder) | ||
|
|
||
| // Prepare all processes for shutdown (prevents auto-restart) | ||
| for _, proc := range shutdownOrder { | ||
| proc.prepareForShutDown() | ||
| } | ||
|
|
||
| // Stop all processes | ||
| p.shutDownAndWait(shutdownOrder) |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The restartAllMutex is used to protect the isRestartingAll flag, but there's a potential race condition window. Between lines 601 and 609, the runProcMutex is released but processes haven't been shut down yet. If another operation tries to interact with processes during this window, it might see inconsistent state. Consider holding the runProcMutex for the entire shutdown phase, or document why this early release is safe.
| // Clear the restart flag on error | ||
| p.restartAllMutex.Lock() | ||
| p.isRestartingAll = false | ||
| p.restartAllMutex.Unlock() | ||
| return fmt.Errorf("failed to build project run order: %w", err) |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an error occurs while building the run order (line 641), the restart flag is properly cleared, but the system is left in an inconsistent state - all processes have been stopped but won't be restarted. Consider whether partial recovery is possible (e.g., attempting to restart processes that were successfully configured) or if this error scenario should trigger a more graceful degradation.
| // Clear the restart flag on error | |
| p.restartAllMutex.Lock() | |
| p.isRestartingAll = false | |
| p.restartAllMutex.Unlock() | |
| return fmt.Errorf("failed to build project run order: %w", err) | |
| // If we could not collect any process at all, treat this as fatal | |
| if len(runOrder) == 0 { | |
| // Clear the restart flag on error | |
| p.restartAllMutex.Lock() | |
| p.isRestartingAll = false | |
| p.restartAllMutex.Unlock() | |
| return fmt.Errorf("failed to build project run order: %w", err) | |
| } | |
| // Otherwise, log the error but continue with the processes we did collect | |
| log.Error().Err(err).Msg("failed to fully build project run order; proceeding with partial run order") |
| for name, observer := range pv.allLogsObservers { | ||
| if err := pv.project.UnSubscribeLogger(name, observer); err != nil { | ||
| log.Err(err).Msgf("failed to unsubscribe from logs for process %s", name) | ||
| } | ||
| } | ||
| pv.allLogsObservers = make(map[string]*AllLogsObserver) |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When unsubscribing from all logs, if an error occurs for one process, the loop continues and recreates the allLogsObservers map, potentially losing track of observers that haven't been unsubscribed yet. Consider collecting all errors and ensuring all observers are attempted to be unsubscribed before recreating the map, or handling partial cleanup more gracefully.
| for name, observer := range pv.allLogsObservers { | |
| if err := pv.project.UnSubscribeLogger(name, observer); err != nil { | |
| log.Err(err).Msgf("failed to unsubscribe from logs for process %s", name) | |
| } | |
| } | |
| pv.allLogsObservers = make(map[string]*AllLogsObserver) | |
| failedObservers := make(map[string]*AllLogsObserver) | |
| for name, observer := range pv.allLogsObservers { | |
| if err := pv.project.UnSubscribeLogger(name, observer); err != nil { | |
| log.Err(err).Msgf("failed to unsubscribe from logs for process %s", name) | |
| failedObservers[name] = observer | |
| } | |
| } | |
| pv.allLogsObservers = failedObservers |
| // Only show process-specific shortcuts when NOT in all-logs mode | ||
| if !pv.allLogsMode { | ||
| pv.shortcuts.addCategory("PROCESS:", pv.helpFooter) | ||
| pv.shortcuts.addButton(ActionProcessScale, pv.helpFooter) | ||
| pv.shortcuts.addButton(ActionProcessInfo, pv.helpFooter) | ||
| pv.shortcuts.addButton(ActionProcessStart, pv.helpFooter) | ||
| pv.shortcuts.addToggleButton(ActionProcessScreen, pv.helpFooter, procScrBool) | ||
| pv.shortcuts.addButton(ActionProcessStop, pv.helpFooter) | ||
| pv.shortcuts.addButton(ActionProcessRestart, pv.helpFooter) | ||
| } |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When in all-logs mode, it might be beneficial to show the ActionProcessRestartAll shortcut in the help footer, since viewing all logs together is a natural context for restarting all processes. Currently, no process-specific actions are shown in all-logs mode, which means users won't see the "Restart All" shortcut even though it would be most useful in this context. Consider adding ActionProcessRestartAll to the help footer when allLogsMode is true.
| // followAllLogs subscribes to logs from all processes and displays them with process name prefixes. | ||
| func (pv *pcView) followAllLogs() { | ||
| pv.logsText.Clear() | ||
| pv.logsText.useAnsi = false // Process name prefixes use tview color tags |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting useAnsi to false when entering all-logs mode changes the log rendering behavior for all processes. This means ANSI escape sequences in process logs will be escaped and displayed as text rather than interpreted. This is inconsistent with the single-process log view behavior where ANSI is used. Consider whether this is intentional, and if so, document why. If ANSI should be supported in all-logs mode, you'll need a different approach to color the process name prefixes (perhaps using ANSI codes instead of tview color tags).
| // Clear done processes map | ||
| p.doneProcMutex.Lock() | ||
| p.doneProcesses = make(map[string]*Process) |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clearing the doneProcesses map during restart-all could cause issues if there are references to these Process objects elsewhere in the code. Before clearing, verify that all observers and references to done processes are properly cleaned up. Consider whether the processes in doneProcesses should be explicitly cleaned up before clearing the map to avoid potential resource leaks.
| // Clear done processes map | |
| p.doneProcMutex.Lock() | |
| p.doneProcesses = make(map[string]*Process) | |
| // Clear done processes map (in-place to avoid changing map identity) | |
| p.doneProcMutex.Lock() | |
| for name := range p.doneProcesses { | |
| delete(p.doneProcesses, name) | |
| } |
| state.Status = types.ProcessStatePending | ||
| state.Pid = 0 | ||
| state.ExitCode = 0 | ||
| state.IsRunning = false |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The process state reset sets all states to Pending and clears runtime information, but it doesn't reset other fields like SystemTime, Health, Mem, CPU, or Restarts. This could lead to stale information being displayed in the UI immediately after restart. Consider whether these fields should also be reset, or if there's a mechanism to refresh them when processes start.
| state.IsRunning = false | |
| state.IsRunning = false | |
| state.SystemTime = "" | |
| state.Health = "" | |
| state.Mem = 0 | |
| state.CPU = 0 | |
| state.Restarts = 0 |
| func (pv *pcView) handleRestartAll() { | ||
| pv.appView.QueueUpdateDraw(func() { | ||
| m := tview.NewModal(). | ||
| SetText("Restart all processes?\nThis will stop all running processes and start them again."). | ||
| AddButtons([]string{"Yes", "No"}). | ||
| SetDoneFunc(func(buttonIndex int, buttonLabel string) { | ||
| pv.pages.SwitchToPage(PageMain) | ||
| pv.pages.RemovePage(PageDialog) | ||
| if buttonLabel == "Yes" { | ||
| go pv.executeRestartAll() | ||
| } | ||
| }) |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The modal dialog for restart-all uses the same vim-like keybindings (h, j, k, l) as other dialogs in the codebase, which is good for consistency. However, the dialog text could be more descriptive about what "restart" means in this context (stops all processes in order, then starts them again in order). Consider adding this detail to help users understand the operation's scope and impact.


When developing it is often nice to be able to see all concurrent processes logs together when evaluating behavior or diagnosing an issue. This PR allows the user to see a consolidated view of the processes logs similar to docker compose output.
The functionality implemented allows the table header of the process list to be selected. When this row is selected the output of all the processes is displayed in a single view. Other considerations like shortcuts dependent on the selected process are handled appropriately.