Files
picoclaw/web/backend/systray.go
T
wenjie 8a44410e37 feat: add web gateway hot reload and polling state sync (#1684)
* feat(gateway): support hot reload and empty startup

- extract gateway runtime into pkg/gateway
- add gateway.hot_reload config with default and example values
- allow starting the gateway without a default model via --allow-empty
- stop treating missing enabled channels as a startup error
- update related tests

* feat: replace gateway SSE updates with polling-based state sync

- remove gateway SSE broadcasting and event endpoint
- add polling-based gateway status refresh with stopping state handling
- detect when gateway restart is required after default model changes
- resolve gateway health and websocket proxy targets from configured host
- update gateway UI labels and add backend/frontend test coverage
2026-03-17 18:46:00 +08:00

134 lines
3.4 KiB
Go

package main
import (
"context"
_ "embed"
"fmt"
"time"
"fyne.io/systray"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/web/backend/utils"
)
const (
browserDelay = 500 * time.Millisecond
shutdownTimeout = 15 * time.Second
)
// onReady is called when the system tray is ready
func onReady() {
// Set icon and tooltip
systray.SetIcon(getIcon())
systray.SetTooltip(fmt.Sprintf(T(AppTooltip), appName))
// Create menu items
mOpen := systray.AddMenuItem(T(MenuOpen), T(MenuOpenTooltip))
mAbout := systray.AddMenuItem(T(MenuAbout), T(MenuAboutTooltip))
// Add version info under About menu
mVersion := mAbout.AddSubMenuItem(fmt.Sprintf(T(MenuVersion), appVersion), T(MenuVersionTooltip))
mVersion.Disable()
mRepo := mAbout.AddSubMenuItem(T(MenuGitHub), "")
mDocs := mAbout.AddSubMenuItem(T(MenuDocs), "")
systray.AddSeparator()
// Add restart option
mRestart := systray.AddMenuItem(T(MenuRestart), T(MenuRestartTooltip))
systray.AddSeparator()
// Quit option
mQuit := systray.AddMenuItem(T(MenuQuit), T(MenuQuitTooltip))
// Handle menu clicks
go func() {
for {
select {
case <-mOpen.ClickedCh:
if err := openBrowser(); err != nil {
logger.Errorf("Failed to open browser: %v", err)
}
case <-mVersion.ClickedCh:
// Version info - do nothing, just shows current version
case <-mRepo.ClickedCh:
if err := utils.OpenBrowser("https://github.com/sipeed/picoclaw"); err != nil {
logger.Errorf("Failed to open GitHub: %v", err)
}
case <-mDocs.ClickedCh:
if err := utils.OpenBrowser(T(DocUrl)); err != nil {
logger.Errorf("Failed to open docs: %v", err)
}
case <-mRestart.ClickedCh:
fmt.Println("Restart request received...")
if apiHandler != nil {
if pid, err := apiHandler.RestartGateway(); err != nil {
logger.Errorf("Failed to restart gateway: %v", err)
} else {
logger.Infof("Gateway restarted (PID: %d)", pid)
}
}
case <-mQuit.ClickedCh:
systray.Quit()
}
}
}()
if !*noBrowser {
// Auto-open browser after systray is ready (if not disabled)
// Check no-browser flag via environment or pass as parameter if needed
if err := openBrowser(); err != nil {
logger.Errorf("Warning: Failed to auto-open browser: %v", err)
}
}
}
// onExit is called when the system tray is exiting
func onExit() {
fmt.Println(T(Exiting))
// First, shutdown API handler
if apiHandler != nil {
apiHandler.Shutdown()
}
if server != nil {
// Disable keep-alive to allow graceful shutdown
server.SetKeepAlivesEnabled(false)
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
// Context deadline exceeded is expected if there are active connections
// This is not necessarily an error, so log it at info level
if err == context.DeadlineExceeded {
logger.Infof("Server shutdown timeout after %v, forcing close", shutdownTimeout)
} else {
logger.Errorf("Server shutdown error: %v", err)
}
} else {
logger.Infof("Server shutdown completed successfully")
}
}
}
// openBrowser opens the PicoClaw web console in the default browser
func openBrowser() error {
if serverAddr == "" {
return fmt.Errorf("server address not set")
}
return utils.OpenBrowser(serverAddr)
}
// getIcon returns the system tray icon
func getIcon() []byte {
return iconData
}