Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@cbergoon
Copy link

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.

- 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.
Copilot AI review requested due to automatic review settings January 24, 2026 04:23
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
12.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link

Copilot AI left a 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.

Comment on lines 269 to 279
})
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()
Copy link

Copilot AI Jan 24, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 269 to 279
})
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()
Copy link

Copilot AI Jan 24, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +601 to +615
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)
Copy link

Copilot AI Jan 24, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +643 to +647
// 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)
Copy link

Copilot AI Jan 24, 2026

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.

Suggested change
// 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")

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +168
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)
Copy link

Copilot AI Jan 24, 2026

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +533 to +542
// 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)
}
Copy link

Copilot AI Jan 24, 2026

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.

Copilot uses AI. Check for mistakes.
// 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
Copy link

Copilot AI Jan 24, 2026

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).

Copilot uses AI. Check for mistakes.
Comment on lines +617 to +619
// Clear done processes map
p.doneProcMutex.Lock()
p.doneProcesses = make(map[string]*Process)
Copy link

Copilot AI Jan 24, 2026

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.

Suggested change
// 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)
}

Copilot uses AI. Check for mistakes.
state.Status = types.ProcessStatePending
state.Pid = 0
state.ExitCode = 0
state.IsRunning = false
Copy link

Copilot AI Jan 24, 2026

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.

Suggested change
state.IsRunning = false
state.IsRunning = false
state.SystemTime = ""
state.Health = ""
state.Mem = 0
state.CPU = 0
state.Restarts = 0

Copilot uses AI. Check for mistakes.
Comment on lines +567 to +578
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()
}
})
Copy link

Copilot AI Jan 24, 2026

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.

Copilot uses AI. Check for mistakes.
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