Wails v3 Tray Binary Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the sap-devs-tray binary — a Wails v3 application that provides a system tray icon with right-click menu and a Fiori-themed webview dashboard panel.
Architecture: A separate Go module at cmd/sap-devs-tray/ with its own go.mod importing Wails v3. The app registers a system tray, starts an embedded HTTP server (bound to 127.0.0.1, session-token-protected), and opens a webview popup panel on left-click. Frontend uses SAP Fundamental Styles with sap_horizon/sap_horizon_dark themes, auto-switching via prefers-color-scheme.
Tech Stack: Wails v3 (alpha), SAP Fundamental Styles CSS, Go embed.FS, net/http
Spec: docs/superpowers/specs/2026-04-20-system-tray-design.md
Prerequisites: Plans 1 (OS scheduler) and 2 (tray lifecycle) should be completed first so that sap-devs service status and shared state files exist.
Important: This plan builds a separate Go module. All go commands in this plan run from cmd/sap-devs-tray/, not the repo root.
Task 1: Initialize the Wails v3 module
Files:
Create:
cmd/sap-devs-tray/go.modCreate:
cmd/sap-devs-tray/main.go[ ] Step 1: Create the module directory
mkdir -p cmd/sap-devs-tray- [ ] Step 2: Initialize go.mod
cd cmd/sap-devs-tray
go mod init github.com/SAP-samples/sap-devs-cli/cmd/sap-devs-tray- [ ] Step 3: Add Wails v3 dependency
cd cmd/sap-devs-tray
go get github.com/wailsapp/wails/v3@latestNote: Pin to a specific alpha tag once confirmed working (e.g., @v3.0.0-alpha.77). Update the tag in go.mod after testing.
- [ ] Step 4: Write minimal main.go
Create cmd/sap-devs-tray/main.go:
package main
import (
"fmt"
"os"
)
var version = "dev"
func main() {
if len(os.Args) > 1 && os.Args[1] == "--version" {
fmt.Printf("sap-devs-tray %s\n", version)
os.Exit(0)
}
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func run() error {
// Wails app initialization will go here
fmt.Println("sap-devs-tray starting... (skeleton)")
select {} // block forever for now
}- [ ] Step 5: Verify it builds
cd cmd/sap-devs-tray && go build -o sap-devs-tray .
./sap-devs-tray --versionExpected: sap-devs-tray dev
- [ ] Step 6: Commit
git add cmd/sap-devs-tray/go.mod cmd/sap-devs-tray/go.sum cmd/sap-devs-tray/main.go
git commit -m "feat(tray-binary): initialize Wails v3 module with skeleton main"Task 2: Shared state reader
Files:
Create:
cmd/sap-devs-tray/state.go[ ] Step 1: Write the state reader
This reads the JSON state files written by the main CLI. Create cmd/sap-devs-tray/state.go:
package main
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"time"
"gopkg.in/yaml.v3"
)
// DashboardState is the combined state served to the frontend via /api/state.
type DashboardState struct {
Version string `json:"version"`
Profile ProfileState `json:"profile"`
Sync SyncState `json:"sync"`
Tools []ToolState `json:"tools"`
ServiceUp bool `json:"serviceUp"`
}
type ProfileState struct {
ID string `json:"id"`
Name string `json:"name"`
Packs []string `json:"packs"`
}
type SyncState struct {
LastSynced time.Time `json:"lastSynced"`
NextSync time.Time `json:"nextSync"`
PackCount int `json:"packCount"`
Status string `json:"status"` // "up_to_date", "stale", "syncing", "error"
}
type ToolState struct {
Name string `json:"name"`
Injected bool `json:"injected"`
}
// ReadState assembles the dashboard state from shared filesystem state.
func ReadState(configDir, cacheDir string) *DashboardState {
state := &DashboardState{
Version: version,
Sync: readSyncState(cacheDir),
Profile: readProfile(configDir),
}
return state
}
func readSyncState(cacheDir string) SyncState {
path := filepath.Join(cacheDir, "sync-state.json")
data, err := os.ReadFile(path)
if err != nil {
return SyncState{Status: "unknown"}
}
var raw map[string]time.Time
if err := json.Unmarshal(data, &raw); err != nil {
return SyncState{Status: "unknown"}
}
var latest time.Time
count := 0
for _, t := range raw {
count++
if t.After(latest) {
latest = t
}
}
st := SyncState{
LastSynced: latest,
PackCount: count,
Status: "up_to_date",
}
if time.Since(latest) > 12*time.Hour {
st.Status = "stale"
}
return st
}
func readProfile(configDir string) ProfileState {
path := filepath.Join(configDir, "profile.yaml")
data, err := os.ReadFile(path)
if err != nil {
return ProfileState{ID: "unknown"}
}
var p struct {
ID string `yaml:"id"`
}
if err := yaml.Unmarshal(data, &p); err != nil || p.ID == "" {
return ProfileState{ID: "unknown"}
}
return ProfileState{ID: p.ID, Name: profileDisplayName(p.ID)}
}
func profileDisplayName(id string) string {
names := map[string]string{
"cap-developer": "CAP Developer",
"abap-developer": "ABAP Developer",
"btp-developer": "BTP Developer",
"all": "All Packs",
"minimal": "Minimal",
}
if name, ok := names[id]; ok {
return name
}
return id
}
func defaultConfigDir() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("APPDATA"), "sap-devs")
case "darwin":
home, _ := os.UserHomeDir()
return filepath.Join(home, "Library", "Application Support", "sap-devs")
default:
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "sap-devs")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "sap-devs")
}
}
func defaultCacheDir() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("LOCALAPPDATA"), "sap-devs", "cache")
case "darwin":
home, _ := os.UserHomeDir()
return filepath.Join(home, "Library", "Caches", "sap-devs")
default:
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return filepath.Join(xdg, "sap-devs")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".cache", "sap-devs")
}
}- [ ] Step 2: Verify it builds
cd cmd/sap-devs-tray && go build ./...- [ ] Step 3: Commit
git add cmd/sap-devs-tray/state.go
git commit -m "feat(tray-binary): add shared state reader for dashboard"Task 3: Embedded HTTP server with session token
Files:
Create:
cmd/sap-devs-tray/server.go[ ] Step 1: Write the server
Create cmd/sap-devs-tray/server.go:
package main
import (
"crypto/rand"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/http"
"os/exec"
"runtime"
"strings"
)
//go:embed frontend
var frontendFS embed.FS
// Server is the embedded HTTP server that serves the frontend and API.
type Server struct {
Token string
ConfigDir string
CacheDir string
listener net.Listener
mux *http.ServeMux
}
func NewServer(configDir, cacheDir string) (*Server, error) {
token, err := generateToken()
if err != nil {
return nil, err
}
s := &Server{
Token: token,
ConfigDir: configDir,
CacheDir: cacheDir,
mux: http.NewServeMux(),
}
frontendContent, _ := fs.Sub(frontendFS, "frontend")
s.mux.Handle("/", http.FileServer(http.FS(frontendContent)))
s.mux.HandleFunc("/api/state", s.requireToken(s.handleState))
s.mux.HandleFunc("/api/sync", s.requireToken(s.handleSync))
s.mux.HandleFunc("/api/inject", s.requireToken(s.handleInject))
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
s.listener = listener
return s, nil
}
func (s *Server) Port() int {
return s.listener.Addr().(*net.TCPAddr).Port
}
func (s *Server) URL() string {
return fmt.Sprintf("http://127.0.0.1:%d", s.Port())
}
func (s *Server) PanelURL() string {
return fmt.Sprintf("%s/?token=%s", s.URL(), s.Token)
}
func (s *Server) Start() error {
return http.Serve(s.listener, s.mux)
}
func (s *Server) requireToken(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
token = strings.TrimPrefix(auth, "Bearer ")
}
}
if token != s.Token {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
func (s *Server) handleState(w http.ResponseWriter, r *http.Request) {
state := ReadState(s.ConfigDir, s.CacheDir)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(state)
}
func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) {
go func() {
cmd := exec.Command(sapDevsBinary(), "sync")
cmd.Stdout = nil
cmd.Stderr = nil
_ = cmd.Run()
}()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "started"})
}
func (s *Server) handleInject(w http.ResponseWriter, r *http.Request) {
go func() {
cmd := exec.Command(sapDevsBinary(), "inject", "--no-sync")
cmd.Stdout = nil
cmd.Stderr = nil
_ = cmd.Run()
}()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "started"})
}
func sapDevsBinary() string {
name := "sap-devs"
if runtime.GOOS == "windows" {
name = "sap-devs.exe"
}
// Look in PATH first; the CLI should be installed already
if path, err := exec.LookPath(name); err == nil {
return path
}
return name
}
func generateToken() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}- [ ] Step 2: Verify it builds
cd cmd/sap-devs-tray && go build ./...Expected: May fail if frontend/ directory doesn't exist yet — create a placeholder:
mkdir -p cmd/sap-devs-tray/frontend
echo "<html><body>placeholder</body></html>" > cmd/sap-devs-tray/frontend/index.html- [ ] Step 3: Commit
git add cmd/sap-devs-tray/server.go cmd/sap-devs-tray/frontend/index.html
git commit -m "feat(tray-binary): add embedded HTTP server with session token auth"Task 4: Frontend — Fiori dashboard with Fundamental Styles
Files:
Create:
cmd/sap-devs-tray/frontend/index.htmlCreate:
cmd/sap-devs-tray/frontend/css/app.cssCreate:
cmd/sap-devs-tray/frontend/js/app.js[ ] Step 1: Download Fundamental Styles CSS
cd cmd/sap-devs-tray/frontend/css
# Download fundamental-styles and theming CSS variables
# These will be embedded in the binary — no CDN at runtime
curl -o fundamental-styles.min.css "https://unpkg.com/fundamental-styles@latest/dist/fundamental-styles.css"
curl -o sap_horizon.css "https://unpkg.com/@sap-theming/theming-base-content/content/Base/baseLib/sap_horizon/css_variables.css"
curl -o sap_horizon_dark.css "https://unpkg.com/@sap-theming/theming-base-content/content/Base/baseLib/sap_horizon_dark/css_variables.css"- [ ] Step 2: Write the dashboard HTML
Replace cmd/sap-devs-tray/frontend/index.html with the full Fiori dashboard. This should use fd-* Fundamental Styles classes for buttons, cards, object-status, etc. Include:
<link>to the CSS files (relative paths, served by embedded FS)- A
<script>block orjs/app.jsreference that:- Reads the
tokenfrom the URL query parameter - Polls
/api/state?token=<token>every 30 seconds - Renders sync status, profile, injected tools
- Handles Sync Now / Inject Now button clicks
- Reads the
@media (prefers-color-scheme: dark)to switch betweensap_horizonandsap_horizon_darkvariable imports
The HTML structure should match the mockup approved during brainstorming (header, sync status card, profile card, tools card, action buttons).
- [ ] Step 3: Write app.css
Create cmd/sap-devs-tray/frontend/css/app.css with panel-specific overrides:
:root {
--panel-width: 400px;
--panel-max-height: 550px;
}
body {
margin: 0;
padding: 0;
width: var(--panel-width);
max-height: var(--panel-max-height);
overflow-y: auto;
font-family: '72', 'Segoe UI', system-ui, sans-serif;
}
.panel-header {
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.status-card, .profile-card, .tools-card {
margin: 0 16px 12px;
padding: 14px;
border-radius: 12px;
}
.action-buttons {
margin: 0 16px 16px;
display: flex;
gap: 8px;
}
.action-buttons .fd-button {
flex: 1;
}- [ ] Step 4: Write app.js
Create cmd/sap-devs-tray/frontend/js/app.js:
(function() {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
async function fetchState() {
try {
const resp = await fetch(`/api/state?token=${token}`);
if (!resp.ok) return;
const state = await resp.json();
renderState(state);
} catch (e) {
console.error('Failed to fetch state:', e);
}
}
function renderState(state) {
// Update sync status
const lastSynced = state.sync.lastSynced
? timeAgo(new Date(state.sync.lastSynced))
: 'Never';
document.getElementById('last-synced').textContent = lastSynced;
document.getElementById('pack-count').textContent = state.sync.packCount || '—';
document.getElementById('sync-status-badge').textContent =
state.sync.status === 'up_to_date' ? 'Up to Date' : 'Stale';
// Update profile
document.getElementById('profile-name').textContent = state.profile.name || state.profile.id;
document.getElementById('profile-packs').textContent =
(state.profile.packs || []).join(', ') || '—';
// Update version
document.getElementById('version').textContent = 'v' + state.version;
}
function timeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return minutes + 'm ago';
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + 'h ago';
const days = Math.floor(hours / 24);
return days + 'd ago';
}
// Action handlers
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('btn-sync')?.addEventListener('click', async () => {
await fetch(`/api/sync?token=${token}`, { method: 'POST' });
setTimeout(fetchState, 2000);
});
document.getElementById('btn-inject')?.addEventListener('click', async () => {
await fetch(`/api/inject?token=${token}`, { method: 'POST' });
setTimeout(fetchState, 2000);
});
fetchState();
setInterval(fetchState, 30000);
});
})();- [ ] Step 5: Verify it builds
cd cmd/sap-devs-tray && go build ./...- [ ] Step 6: Commit
git add cmd/sap-devs-tray/frontend/
git commit -m "feat(tray-binary): add Fiori dashboard with Fundamental Styles theming"Task 5: Wails v3 app — tray icon and webview panel
Files:
Create:
cmd/sap-devs-tray/app.goModify:
cmd/sap-devs-tray/main.go[ ] Step 1: Write app.go — Wails v3 tray setup
Create cmd/sap-devs-tray/app.go. Based on the Wails v3 systray-menu example (v3/examples/systray-menu/main.go), the API uses app.SystemTray.New(), app.Window.NewWithOptions(), and app.NewMenu(). Pin the Wails v3 alpha version after confirming this compiles.
package main
import (
_ "embed"
"fmt"
"os"
"os/exec"
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
//go:embed frontend/icon.png
var trayIcon []byte
func startApp(srv *Server) error {
app := application.New(application.Options{
Name: "sap-devs",
Description: "SAP Developer Companion",
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
},
})
// Create the webview panel window (hidden by default)
panel := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "sap-devs Dashboard",
Width: 400,
Height: 550,
URL: srv.PanelURL(),
Frameless: true,
AlwaysOnTop: true,
Hidden: true,
DisableResize: true,
Windows: application.WindowsWindow{
HiddenOnTaskbar: true,
},
})
// Hide instead of closing the panel window
panel.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
panel.Hide()
e.Cancel()
})
// Create system tray
systemTray := app.SystemTray.New()
systemTray.SetIcon(trayIcon)
systemTray.SetTooltip("sap-devs")
// Build tray menu
menu := app.NewMenu()
menu.Add(fmt.Sprintf("sap-devs %s", version)).SetEnabled(false)
menu.AddSeparator()
menu.Add("Sync Now").OnClick(func(ctx *application.Context) {
go func() {
cmd := exec.Command(sapDevsBinary(), "sync")
_ = cmd.Run()
}()
})
menu.Add("Inject Now").OnClick(func(ctx *application.Context) {
go func() {
cmd := exec.Command(sapDevsBinary(), "inject", "--no-sync")
_ = cmd.Run()
}()
})
menu.AddSeparator()
menu.Add("Open Dashboard...").OnClick(func(ctx *application.Context) {
panel.Show()
panel.Focus()
})
menu.Add("Open Terminal...").OnClick(func(ctx *application.Context) {
openTerminal()
})
menu.AddSeparator()
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systemTray.SetMenu(menu)
systemTray.AttachWindow(panel).WindowOffset(2)
return app.Run()
}
func openTerminal() {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("powershell", "-NoExit")
case "darwin":
cmd = exec.Command("open", "-a", "Terminal")
default:
// Try $TERMINAL, then x-terminal-emulator, then xterm
if term := envOr("TERMINAL", ""); term != "" {
cmd = exec.Command(term)
} else if path, err := exec.LookPath("x-terminal-emulator"); err == nil {
cmd = exec.Command(path)
} else {
cmd = exec.Command("xterm")
}
}
_ = cmd.Start()
}
func envOr(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}Note: The exact Wails v3 API may shift between alpha releases. If app.SystemTray.New() or app.Window.NewWithOptions() signatures change, consult the Wails v3 examples at github.com/wailsapp/wails/tree/v3-alpha/v3/examples/systray-menu/main.go for the current API.
- [ ] Step 2: Update main.go to wire everything
Update cmd/sap-devs-tray/main.go run() function:
func run() error {
configDir := defaultConfigDir()
cacheDir := defaultCacheDir()
srv, err := NewServer(configDir, cacheDir)
if err != nil {
return fmt.Errorf("could not start server: %w", err)
}
go srv.Start()
return startApp(srv)
}Where startApp(srv) is the function in app.go that creates the Wails application with the tray and webview.
- [ ] Step 3: Add a tray icon
Create or download an SVG/PNG icon for the tray. Store at cmd/sap-devs-tray/frontend/icon.png (or embed directly). The icon should be the sap-devs logo, small enough for system tray (~16x16 or ~22x22 depending on platform).
- [ ] Step 4: Verify it builds and runs
cd cmd/sap-devs-tray && go build -o sap-devs-tray . && ./sap-devs-trayExpected: Tray icon appears in system tray. Right-click shows menu. Left-click opens webview panel.
- [ ] Step 5: Commit
git add cmd/sap-devs-tray/app.go cmd/sap-devs-tray/main.go cmd/sap-devs-tray/frontend/icon.png
git commit -m "feat(tray-binary): wire Wails v3 system tray with webview panel"Task 6: CI build configuration
Files:
Create:
.github/workflows/release-tray.yml[ ] Step 1: Write the CI workflow
Create .github/workflows/release-tray.yml:
name: Release Tray Binary
on:
release:
types: [published]
permissions:
contents: write
jobs:
build-tray:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
- os: ubuntu-latest
goos: linux
goarch: arm64
cc: aarch64-linux-gnu-gcc
- os: macos-latest
goos: darwin
goarch: arm64
cc: ""
- os: macos-13
goos: darwin
goarch: amd64
cc: ""
- os: windows-latest
goos: windows
goarch: amd64
cc: ""
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: cmd/sap-devs-tray/go.mod
- name: Install Linux amd64 deps
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
- name: Install Linux arm64 cross-compile deps
if: matrix.goos == 'linux' && matrix.goarch == 'arm64'
run: |
sudo dpkg --add-architecture arm64
sudo apt-get update
sudo apt-get install -y crossbuild-essential-arm64 \
libgtk-3-dev:arm64 libwebkit2gtk-4.1-dev:arm64
- name: Build
working-directory: cmd/sap-devs-tray
env:
CGO_ENABLED: "1"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CC: ${{ matrix.cc }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
EXT=""
if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; fi
go build -ldflags "-X main.version=${VERSION}" -o "sap-devs-tray${EXT}" .
- name: Package
run: |
VERSION="${GITHUB_REF_NAME#v}"
ASSET="sap-devs-tray_${VERSION}_${{ matrix.goos }}_${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
zip "${ASSET}.zip" -j cmd/sap-devs-tray/sap-devs-tray.exe
else
tar czf "${ASSET}.tar.gz" -C cmd/sap-devs-tray sap-devs-tray
fi
- name: Generate checksum
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
ASSET="sap-devs-tray_${VERSION}_${{ matrix.goos }}_${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
FILE="${ASSET}.zip"
else
FILE="${ASSET}.tar.gz"
fi
if command -v sha256sum &>/dev/null; then
sha256sum "${FILE}" > "${FILE}.sha256"
else
shasum -a 256 "${FILE}" > "${FILE}.sha256"
fi
- name: Upload to Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
ASSET="sap-devs-tray_${VERSION}_${{ matrix.goos }}_${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
gh release upload "${GITHUB_REF_NAME}" "${ASSET}.zip" "${ASSET}.zip.sha256" --clobber
else
gh release upload "${GITHUB_REF_NAME}" "${ASSET}.tar.gz" "${ASSET}.tar.gz.sha256" --clobber
fi
aggregate-checksums:
needs: build-tray
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download per-artifact checksums
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p checksums
for suffix in linux_amd64.tar.gz linux_arm64.tar.gz darwin_arm64.tar.gz darwin_amd64.tar.gz windows_amd64.zip; do
FILE="sap-devs-tray_${VERSION}_${suffix}.sha256"
gh release download "${GITHUB_REF_NAME}" -p "${FILE}" -D checksums || true
done
- name: Aggregate into checksums.txt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cat checksums/*.sha256 > tray-checksums.txt
gh release upload "${GITHUB_REF_NAME}" tray-checksums.txt --clobber- [ ] Step 2: Commit
git add .github/workflows/release-tray.yml
git commit -m "ci: add tray binary build and release workflow"Task 7: Documentation and CLAUDE.md update
Files:
Modify:
CLAUDE.md[ ] Step 1: Add tray binary section to CLAUDE.md Architecture Overview
Add after the "### Sync" section:
### Tray Companion (Optional)
`sap-devs-tray` is an optional GUI companion built with Wails v3 (alpha). It lives in `cmd/sap-devs-tray/` with its own `go.mod` — the main CLI never imports Wails. The tray binary provides a system tray icon and a Fiori-themed webview dashboard panel using SAP Fundamental Styles (`sap_horizon` / `sap_horizon_dark` themes, auto-switching via OS preference).
**Architecture:** The tray reads shared state files (`sync-state.json`, `config.yaml`) written by the main CLI. An embedded HTTP server bound to `127.0.0.1` (session-token-protected) serves the dashboard frontend. The main CLI manages the tray lifecycle via `internal/trayctl/` (download from GitHub Releases, start/stop, autostart registration).
**Alpha disclaimer:** Wails v3 is in alpha. The tray is strictly optional — all CLI features work without it. If Wails v3 breaks, only the tray binary is affected.- [ ] Step 2: Commit
git add CLAUDE.md
git commit -m "docs: add tray binary architecture to CLAUDE.md"