From 7b4d5d4513bc8669710df808995dcf92a5832c19 Mon Sep 17 00:00:00 2001 From: taorye Date: Fri, 20 Mar 2026 17:58:33 +0800 Subject: [PATCH] feat: add channels management page and integrate into home menu --- cmd/picoclaw-launcher-tui/ui/app.go | 1 + cmd/picoclaw-launcher-tui/ui/channels.go | 194 +++++++++++++++++++++++ cmd/picoclaw-launcher-tui/ui/home.go | 3 + 3 files changed, 198 insertions(+) create mode 100644 cmd/picoclaw-launcher-tui/ui/channels.go diff --git a/cmd/picoclaw-launcher-tui/ui/app.go b/cmd/picoclaw-launcher-tui/ui/app.go index 4978935d9..b410581f9 100644 --- a/cmd/picoclaw-launcher-tui/ui/app.go +++ b/cmd/picoclaw-launcher-tui/ui/app.go @@ -303,6 +303,7 @@ func (a *App) buildShell(pageID string, content tview.Primitive, hint string) tv sbText += menuItem("schemes", "SCHEMES") sbText += menuItem("users", "USERS") sbText += menuItem("models", "MODELS") + sbText += menuItem("channels", "CHANNELS") sidebar.SetText(sbText) diff --git a/cmd/picoclaw-launcher-tui/ui/channels.go b/cmd/picoclaw-launcher-tui/ui/channels.go new file mode 100644 index 000000000..4ba87b617 --- /dev/null +++ b/cmd/picoclaw-launcher-tui/ui/channels.go @@ -0,0 +1,194 @@ +// PicoClaw - Ultra-lightweight personal AI agent +// License: MIT +// +// Copyright (c) 2026 PicoClaw contributors + +package ui + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strconv" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +func (a *App) newChannelsPage() tview.Primitive { + list := tview.NewList() + list.SetBorder(true).SetTitle(" [#00f0ff::b] COMMUNICATION CHANNELS ").SetTitleColor(tcell.NewHexColor(0x00f0ff)).SetBorderColor(tcell.NewHexColor(0x00f0ff)) + list.SetMainTextColor(tcell.NewHexColor(0xe0e0e0)) + list.SetSecondaryTextColor(tcell.NewHexColor(0x808080)) + list.SetSelectedStyle(tcell.StyleDefault.Background(tcell.NewHexColor(0xff00ff)).Foreground(tcell.NewHexColor(0x050510))) + list.SetHighlightFullLine(true) + list.SetBackgroundColor(tcell.NewHexColor(0x050510)) + + rebuild := func() { + sel := list.GetCurrentItem() + list.Clear() + + home, err := os.UserHomeDir() + if err != nil { + home = "." + } + configPath := filepath.Join(home, ".picoclaw", "config.json") + + var cfg map[string]interface{} + if data, err := os.ReadFile(configPath); err == nil { + _ = json.Unmarshal(data, &cfg) + } + + if chRaw, ok := cfg["channels"].(map[string]interface{}); ok { + for name, ch := range chRaw { + chMap, ok := ch.(map[string]interface{}) + enabled := "disabled" + if ok { + if e, ok := chMap["enabled"].(bool); ok && e { + enabled = "enabled" + } + } + list.AddItem(name, fmt.Sprintf("Status: %s", enabled), 0, func() { + a.showChannelEditForm(configPath, name, chMap) + }) + } + } + + if sel >= 0 && sel < list.GetItemCount() { + list.SetCurrentItem(sel) + } + } + rebuild() + + a.pageRefreshFns["channels"] = rebuild + + list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyEscape { + return a.goBack() + } + return event + }) + + return a.buildShell("channels", list, " [#ff00ff]Enter:[-] edit [#ff2a2a]ESC:[-] back ") +} + +func (a *App) showChannelEditForm(configPath, channelName string, existing map[string]interface{}) { + form := tview.NewForm() + form.SetBorder(true).SetTitle(" [::b]EDIT CHANNEL ").SetTitleColor(tcell.NewHexColor(0x39ff14)).SetBorderColor(tcell.NewHexColor(0x00f0ff)) + form.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e)) + form.SetFieldBackgroundColor(tcell.NewHexColor(0x050510)) + form.SetFieldTextColor(tcell.NewHexColor(0x00f0ff)) + form.SetLabelColor(tcell.NewHexColor(0xe0e0e0)) + form.SetButtonBackgroundColor(tcell.NewHexColor(0xff00ff)) + form.SetButtonTextColor(tcell.NewHexColor(0xffffff)) + + fields := make(map[string]*tview.InputField) + var nameField *tview.InputField + + if channelName == "" { + nameField = tview.NewInputField(). + SetLabel("Channel Name"). + SetText(""). + SetFieldWidth(28) + form.AddFormItem(nameField) + } + + for k, v := range existing { + if reflect.ValueOf(v).Kind() == reflect.Map || reflect.ValueOf(v).Kind() == reflect.Slice { + continue + } + valStr := fmt.Sprintf("%v", v) + field := tview.NewInputField(). + SetLabel(k). + SetText(valStr). + SetFieldWidth(28) + form.AddFormItem(field) + fields[k] = field + } + + form.AddButton("SAVE", func() { + var cfg map[string]interface{} + if data, err := os.ReadFile(configPath); err == nil { + if err := json.Unmarshal(data, &cfg); err != nil { + cfg = make(map[string]interface{}) + } + } else { + cfg = make(map[string]interface{}) + } + + if _, ok := cfg["channels"]; !ok { + cfg["channels"] = make(map[string]interface{}) + } + channels, ok := cfg["channels"].(map[string]interface{}) + if !ok { + channels = make(map[string]interface{}) + cfg["channels"] = channels + } + + finalName := channelName + if channelName == "" { + if nameField == nil || nameField.GetText() == "" { + a.showError("Channel name is required") + return + } + finalName = nameField.GetText() + } + + updated := make(map[string]interface{}) + if existing != nil { + for k, v := range existing { + updated[k] = v + } + } + for k, field := range fields { + val := field.GetText() + if val == "true" { + updated[k] = true + } else if val == "false" { + updated[k] = false + } else if num, err := strconv.Atoi(val); err == nil { + updated[k] = num + } else { + updated[k] = val + } + } + + if channelName != "" && finalName != channelName { + delete(channels, channelName) + } + channels[finalName] = updated + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + a.showError(fmt.Sprintf("Failed to save config: %v", err)) + return + } + if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil { + a.showError(fmt.Sprintf("Failed to create config directory: %v", err)) + return + } + if err := os.WriteFile(configPath, data, 0o600); err != nil { + a.showError(fmt.Sprintf("Failed to write config: %v", err)) + return + } + + a.hideModal("channel-edit") + a.goBack() + }) + + form.AddButton("CANCEL", func() { + a.hideModal("channel-edit") + }) + + form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyEscape { + a.hideModal("channel-edit") + return nil + } + return event + }) + + a.showModal("channel-edit", centeredForm(form, 4, 20)) +} diff --git a/cmd/picoclaw-launcher-tui/ui/home.go b/cmd/picoclaw-launcher-tui/ui/home.go index 4e952d534..49524acf1 100644 --- a/cmd/picoclaw-launcher-tui/ui/home.go +++ b/cmd/picoclaw-launcher-tui/ui/home.go @@ -25,6 +25,9 @@ func (a *App) newHomePage() tview.Primitive { list.AddItem("MODEL: "+a.cfg.CurrentModelLabel(), "Select to configure AI model", 'm', func() { a.navigateTo("schemes", a.newSchemesPage()) }) + list.AddItem("CHANNELS: Configure communication channels", "Manage Telegram/Discord/WeChat channels", 'n', func() { + a.navigateTo("channels", a.newChannelsPage()) + }) list.AddItem("QUIT SYSTEM", "Exit PicoClaw Launcher", 'q', func() { a.tapp.Stop() }) if sel >= 0 && sel < list.GetItemCount() { list.SetCurrentItem(sel)