feat(tui): Add configurable Launcher and Gateway process management#909
feat(tui): Add configurable Launcher and Gateway process management#909imguoguo merged 1 commit intosipeed:mainfrom
Conversation
- Implement POSIX-specific gateway process management in gateway_posix.go - Implement Windows-specific gateway process management in gateway_windows.go - Create a menu system in menu.go for user interaction - Develop model management functionality in model.go, including adding, deleting, and testing models - Introduce a style configuration in style.go for consistent UI appearance - Set up the main application entry point in main.go - Update go.mod and go.sum to include necessary dependencies for tcell and tview
There was a problem hiding this comment.
Pull request overview
Adds a new picoclaw-launcher-tui terminal UI to configure ~/.picoclaw/config.json and manage starting/stopping picoclaw gateway, aiming to simplify setup and operation from the terminal.
Changes:
- Introduces a new TUI binary (
cmd/picoclaw-launcher-tui) with menus/forms for model + channel configuration. - Adds cross-platform gateway process detection/stop helpers (POSIX + Windows) and integrates them into the TUI.
- Updates release packaging (
.goreleaser.yaml) and module deps (go.mod/go.sum) fortview/tcell.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
cmd/picoclaw-launcher-tui/main.go |
Entry point for the new TUI binary. |
cmd/picoclaw-launcher-tui/internal/ui/app.go |
Main app state, navigation stack, config save/apply/discard, and gateway/talk launch actions. |
cmd/picoclaw-launcher-tui/internal/ui/menu.go |
Menu table abstraction used throughout the TUI. |
cmd/picoclaw-launcher-tui/internal/ui/style.go |
Global theme + banner view. |
cmd/picoclaw-launcher-tui/internal/ui/model.go |
Model list menu, model edit form, and a “Test” action. |
cmd/picoclaw-launcher-tui/internal/ui/channel.go |
Channel enablement menu + per-channel edit forms. |
cmd/picoclaw-launcher-tui/internal/ui/gateway_posix.go |
POSIX gateway detection/stop helpers. |
cmd/picoclaw-launcher-tui/internal/ui/gateway_windows.go |
Windows gateway detection/stop helpers. |
cmd/picoclaw-launcher-tui/internal/config/store.go |
Config path/load/save helpers for ~/.picoclaw/config.json. |
.goreleaser.yaml |
Adds build + packaging entry for the new TUI binary. |
go.mod / go.sum |
Adds tview/tcell (and transitive deps). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if !strings.HasPrefix(modelID, "openai/") { | ||
| s.showMessage("Unsupported model", "Only openai/* models are supported for test") | ||
| return | ||
| } | ||
| modelName := strings.TrimPrefix(modelID, "openai/") | ||
| endpoint := strings.TrimRight(base, "/") + "/chat/completions" | ||
|
|
||
| payload := fmt.Sprintf( | ||
| `{"model":"%s","messages":[{"role":"user","content":"ping"}],"max_tokens":1}`, | ||
| modelName, |
There was a problem hiding this comment.
The "Test" action currently hard-codes support to openai/* models and also strips the openai/ prefix before sending the request. This is likely incompatible with other supported protocols (e.g., openrouter/*, groq/*, etc.) and even some OpenAI-compatible gateways that expect the full model slug. Consider reusing the existing provider factory (pkg/providers.CreateProviderFromConfig) or at least using the configured protocol/model semantics consistently.
| if !strings.HasPrefix(modelID, "openai/") { | |
| s.showMessage("Unsupported model", "Only openai/* models are supported for test") | |
| return | |
| } | |
| modelName := strings.TrimPrefix(modelID, "openai/") | |
| endpoint := strings.TrimRight(base, "/") + "/chat/completions" | |
| payload := fmt.Sprintf( | |
| `{"model":"%s","messages":[{"role":"user","content":"ping"}],"max_tokens":1}`, | |
| modelName, | |
| endpoint := strings.TrimRight(base, "/") + "/chat/completions" | |
| // Use the configured model identifier as-is so that different | |
| // providers/protocols and gateways that rely on full slugs work. | |
| payload := fmt.Sprintf( | |
| `{"model":"%s","messages":[{"role":"user","content":"ping"}],"max_tokens":1}`, | |
| modelID, |
| _ = logFile.Close() | ||
| return | ||
| } | ||
| _ = logFile.Close() |
There was a problem hiding this comment.
After cmd.Start(), the code never calls cmd.Wait(). On Unix this can leave a zombie process when the gateway exits, and it also means s.gatewayCmd won't reflect an unexpectedly terminated gateway. Consider starting a goroutine to Wait() and update state (or use cmd.Run() if you intend to block).
| _ = logFile.Close() | |
| _ = logFile.Close() | |
| go func(c *exec.Cmd) { | |
| _ = c.Wait() | |
| }(cmd) |
| import "os/exec" | ||
|
|
||
| func isGatewayProcessRunning() bool { | ||
| cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq picoclaw.exe") | ||
| return cmd.Run() == nil |
There was a problem hiding this comment.
On Windows, tasklist returns exit code 0 even when no processes match the filter, so cmd.Run() == nil will almost always report the gateway as running. Capture and inspect the output (or use PowerShell/WMI to query processes) to determine whether a matching process actually exists.
| import "os/exec" | |
| func isGatewayProcessRunning() bool { | |
| cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq picoclaw.exe") | |
| return cmd.Run() == nil | |
| import ( | |
| "bytes" | |
| "os/exec" | |
| ) | |
| func isGatewayProcessRunning() bool { | |
| cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq picoclaw.exe") | |
| output, err := cmd.Output() | |
| if err != nil { | |
| return false | |
| } | |
| return bytes.Contains(output, []byte("picoclaw.exe")) |
| if valid { | ||
| color := tview.Styles.PrimaryTextColor | ||
| return &color | ||
| } | ||
| color := tcell.ColorGray |
There was a problem hiding this comment.
modelStatusColor accepts a selected parameter that is never used. Either remove the parameter to avoid confusion, or use it to visually distinguish the active/default model from other valid models.
| if valid { | |
| color := tview.Styles.PrimaryTextColor | |
| return &color | |
| } | |
| color := tcell.ColorGray | |
| var color tcell.Color | |
| switch { | |
| case !valid: | |
| color = tcell.ColorGray | |
| case selected: | |
| color = tview.Styles.ContrastSecondaryTextColor | |
| default: | |
| color = tview.Styles.PrimaryTextColor | |
| } |
| cmd := exec.Command("sh", "-c", "pgrep -f 'picoclaw\\s+gateway' >/dev/null 2>&1") | ||
| return cmd.Run() == nil | ||
| } | ||
|
|
||
| func stopGatewayProcess() error { | ||
| cmd := exec.Command("sh", "-c", "pkill -f 'picoclaw\\s+gateway' >/dev/null 2>&1") |
There was a problem hiding this comment.
The POSIX gateway process match pattern uses \s inside a pgrep -f regex, but pgrep uses POSIX ERE where \s is not a whitespace class. This likely prevents detecting a running gateway. Use a POSIX class like [[:space:]]+ (or a simpler literal picoclaw gateway) instead of \s+ for both pgrep/pkill commands.
| cmd := exec.Command("sh", "-c", "pgrep -f 'picoclaw\\s+gateway' >/dev/null 2>&1") | |
| return cmd.Run() == nil | |
| } | |
| func stopGatewayProcess() error { | |
| cmd := exec.Command("sh", "-c", "pkill -f 'picoclaw\\s+gateway' >/dev/null 2>&1") | |
| cmd := exec.Command("sh", "-c", "pgrep -f 'picoclaw[[:space:]]+gateway' >/dev/null 2>&1") | |
| return cmd.Run() == nil | |
| } | |
| func stopGatewayProcess() error { | |
| cmd := exec.Command("sh", "-c", "pkill -f 'picoclaw[[:space:]]+gateway' >/dev/null 2>&1") |
| form.AddInputField("Token", cfg.Token, 128, nil, func(text string) { | ||
| cfg.Token = strings.TrimSpace(text) | ||
| }) | ||
| form.AddInputField("Proxy", cfg.Proxy, 128, nil, func(text string) { | ||
| cfg.Proxy = strings.TrimSpace(text) | ||
| }) | ||
| form.AddInputField("Allow From", strings.Join(cfg.AllowFrom, ","), 128, nil, func(text string) { | ||
| cfg.AllowFrom = splitCSV(text) | ||
| }) | ||
| return wrapWithBack(form, s) |
There was a problem hiding this comment.
Most channel form input handlers (e.g., Token/Proxy/Allow From) update config but do not set s.dirty = true, so users can exit without a save prompt and lose changes. Mark the state dirty in these callbacks (similar to the Enabled checkbox) and refresh the main menu if you want the UI to reflect readiness immediately.
| func (s *appState) viewGatewayLog() { | ||
| data, err := os.ReadFile(s.logPath) | ||
| if err != nil { | ||
| s.showMessage("Log not found", "gateway.log not found") | ||
| return | ||
| } | ||
| text := tview.NewTextView() | ||
| text.SetBorder(true).SetTitle("Gateway Log") | ||
| text.SetText(string(data)) | ||
| text.SetDoneFunc(func(key tcell.Key) { |
There was a problem hiding this comment.
viewGatewayLog reads the entire log file into memory with no size limit. Since the log is appended to indefinitely, this can lead to high memory usage or OOM. Consider limiting the read (e.g., last N bytes/lines) or streaming into the TextView.
| } | ||
| _ = stopGatewayProcess() | ||
| cmd := exec.Command("picoclaw", "gateway") | ||
| logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) |
There was a problem hiding this comment.
gateway.log is created with mode 0644. Gateway output can include sensitive data (tokens, request payloads, etc.), so this should likely be owner-readable only (0600) to match how config files are written in this repo.
| logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) | |
| logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) |
| s.app.Suspend(func() { | ||
| cmd := exec.Command("picoclaw", "agent") | ||
| cmd.Stdin = os.Stdin | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| _ = cmd.Run() |
There was a problem hiding this comment.
The TUI shells out to picoclaw by name only. If picoclaw isn't on PATH (common for bundled releases), Start Talk/Gateway will fail. Consider resolving the executable similarly to cmd/picoclaw-launcher (prefer picoclaw in the same directory as the launcher, then fallback to PATH).
| payload := fmt.Sprintf( | ||
| `{"model":"%s","messages":[{"role":"user","content":"ping"}],"max_tokens":1}`, | ||
| modelName, | ||
| ) |
There was a problem hiding this comment.
testModel builds JSON with fmt.Sprintf and string interpolation. If modelName contains quotes or other special characters, this will produce invalid JSON. Prefer marshaling a struct/map with encoding/json to ensure correct escaping.
Changes from upstream: - fix(channel): config cleanup and regex precompile (sipeed#911, sipeed#916) - fix(github_copilot): improve error handling (sipeed#919) - fix(wecom): context leaks and data race fixes (sipeed#914, sipeed#918) - fix(tools): HTTP client caching and response body cleanup (sipeed#940) - feat(tui): Add configurable Launcher and Gateway process management (sipeed#909) - feat(migrate): Update migration system with openclaw support (sipeed#910) - fix(skills): Use registry-backed search for skills discovery (sipeed#929) - build: Add armv6 support to goreleaser (sipeed#905) - docs: Sync READMEs and channel documentation
|
@taorye The TUI launcher with gateway process management is a great addition. Having cross-platform support for both POSIX and Windows with the menu system for model configuration makes the setup experience much smoother. We're building a PicoClaw Dev Group on Discord for contributors to connect and collaborate. If you'd like to join, send an email to |
…ipeed#909) - Implement POSIX-specific gateway process management in gateway_posix.go - Implement Windows-specific gateway process management in gateway_windows.go - Create a menu system in menu.go for user interaction - Develop model management functionality in model.go, including adding, deleting, and testing models - Introduce a style configuration in style.go for consistent UI appearance - Set up the main application entry point in main.go - Update go.mod and go.sum to include necessary dependencies for tcell and tview
…ipeed#909) - Implement POSIX-specific gateway process management in gateway_posix.go - Implement Windows-specific gateway process management in gateway_windows.go - Create a menu system in menu.go for user interaction - Develop model management functionality in model.go, including adding, deleting, and testing models - Introduce a style configuration in style.go for consistent UI appearance - Set up the main application entry point in main.go - Update go.mod and go.sum to include necessary dependencies for tcell and tview
…ipeed#909) - Implement POSIX-specific gateway process management in gateway_posix.go - Implement Windows-specific gateway process management in gateway_windows.go - Create a menu system in menu.go for user interaction - Develop model management functionality in model.go, including adding, deleting, and testing models - Introduce a style configuration in style.go for consistent UI appearance - Set up the main application entry point in main.go - Update go.mod and go.sum to include necessary dependencies for tcell and tview
gateway_posix.go.gateway_windows.go.menu.gofor navigating model options, including adding and selecting models.model.goto handle model configurations, including validation and testing.style.gofor consistent application theming.main.goto run the application.go.modandgo.sumto include necessary dependencies for TUI components.📝 Description
🗣️ Type of Change
🤖 AI Code Generation
🔗 Related Issue
Closes #805
📚 Technical Context (Skip for Docs)
🧪 Test Environment
📸 Evidence (Optional)
Click to view Logs/Screenshots
☑️ Checklist