chore: record journal
This commit is contained in:
parent
8cf1a504d1
commit
29d0cabbd1
@ -54,9 +54,25 @@
|
|||||||
"internal/cli/serve.go",
|
"internal/cli/serve.go",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"notes": "Design-only task completed. The PRD recommends a same-process local web viewer with append-only JSONL request logs and no new frontend build system.",
|
"notes": "Design-only task completed. The PRD recommends a same-process local web viewer with append-only JSONL request logs and no new frontend build system. Follow-up product discussion on 2026-04-18 introduced explicit desktop requirements (launch at login, native menu/tray, native notifications). That direction should be handled as a separate desktop-shell task instead of rewriting this completed web-UI/log-viewer design task.",
|
||||||
"meta": {
|
"meta": {
|
||||||
"recommendedSurface": "local-web-ui",
|
"recommendedSurface": "local-web-ui",
|
||||||
|
"desktopFollowupRecommended": true,
|
||||||
|
"desktopFollowupStack": [
|
||||||
|
"wails",
|
||||||
|
"react",
|
||||||
|
"typescript",
|
||||||
|
"tailwindcss",
|
||||||
|
"react-hook-form",
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
|
"desktopArchitecture": [
|
||||||
|
"internal/config for config IO and validation",
|
||||||
|
"internal/proxy for proxy runtime",
|
||||||
|
"internal/opencode for OpenCode sync",
|
||||||
|
"internal/app as shared application service layer for CLI and GUI",
|
||||||
|
"desktop shell handles window tray menu notification autostart only"
|
||||||
|
],
|
||||||
"protocolScope": [
|
"protocolScope": [
|
||||||
"openai-responses"
|
"openai-responses"
|
||||||
],
|
],
|
||||||
|
|||||||
715
.trellis/tasks/04-18-desktop-shell-gui/prd.md
Normal file
715
.trellis/tasks/04-18-desktop-shell-gui/prd.md
Normal file
@ -0,0 +1,715 @@
|
|||||||
|
# OCSWITCH Desktop Shell Design
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`ocswitch` is currently a Go CLI and local proxy. It already has the core routing, config persistence, and OpenCode sync logic needed for daily use, but it does not yet provide a desktop-native control surface.
|
||||||
|
|
||||||
|
This task defines a desktop-shell architecture for `ocswitch` with these explicit product requirements:
|
||||||
|
|
||||||
|
- launch at login
|
||||||
|
- native tray/menu integration
|
||||||
|
- native notifications
|
||||||
|
- a GUI control panel for configuration and runtime status
|
||||||
|
|
||||||
|
The key constraint is equally explicit:
|
||||||
|
|
||||||
|
- do not rewrite existing Go core logic into desktop-only code
|
||||||
|
|
||||||
|
Instead, this task introduces a shared application service layer so the CLI and desktop GUI can reuse the same workflows.
|
||||||
|
|
||||||
|
## Background And Why This Is A Separate Task
|
||||||
|
|
||||||
|
The completed task `04-17-forwarding-log-viewer-gui` correctly designed a same-process local web log viewer and explicitly excluded Electron or native desktop GUI.
|
||||||
|
|
||||||
|
That task remains valid for its original goal.
|
||||||
|
|
||||||
|
This task exists because product direction has changed. The GUI now needs real desktop capabilities:
|
||||||
|
|
||||||
|
- launch at login
|
||||||
|
- tray/menu presence
|
||||||
|
- notifications
|
||||||
|
- background-style operation
|
||||||
|
|
||||||
|
Those requirements justify a desktop shell and should not be retrofitted into the completed local-web-viewer task.
|
||||||
|
|
||||||
|
## Product Goal
|
||||||
|
|
||||||
|
Provide a desktop-native control surface for `ocswitch` that can:
|
||||||
|
|
||||||
|
1. manage providers, aliases, and sync operations through a GUI
|
||||||
|
2. control and observe local proxy runtime state
|
||||||
|
3. run comfortably as a background desktop utility
|
||||||
|
4. expose tray/menu and notification workflows expected from a resident local tool
|
||||||
|
5. preserve the existing Go codebase as the primary implementation of business rules
|
||||||
|
|
||||||
|
## Primary Requirements
|
||||||
|
|
||||||
|
### Desktop capabilities
|
||||||
|
|
||||||
|
- Launch at login must be supported.
|
||||||
|
- Native tray/menu integration must be supported.
|
||||||
|
- Native notifications must be supported.
|
||||||
|
- The application should be able to behave like a local resident utility, not just a browser page.
|
||||||
|
|
||||||
|
### Reuse requirements
|
||||||
|
|
||||||
|
- `internal/config` must continue to own config structures, loading, saving, and validation.
|
||||||
|
- `internal/proxy` must continue to own proxy runtime and failover behavior.
|
||||||
|
- `internal/opencode` must continue to own OpenCode config sync behavior.
|
||||||
|
- A new `internal/app` layer must own shared workflows used by both CLI and GUI.
|
||||||
|
- The desktop shell must not directly reimplement provider/alias/sync rules.
|
||||||
|
|
||||||
|
### GUI scope
|
||||||
|
|
||||||
|
The first GUI iteration should focus on a control-panel workflow rather than a full analytics product.
|
||||||
|
|
||||||
|
Minimum surface:
|
||||||
|
|
||||||
|
- overview/status page
|
||||||
|
- provider management page
|
||||||
|
- alias management page
|
||||||
|
- doctor and OpenCode sync page
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
1. No rewrite of config/proxy/opencode logic into desktop-specific modules.
|
||||||
|
2. No replacement of the existing CLI.
|
||||||
|
3. No hosted service or remote control plane.
|
||||||
|
4. No multi-user or authenticated remote administration model.
|
||||||
|
5. No forced migration away from existing config file formats.
|
||||||
|
6. No requirement that the initial desktop shell also ships the full request log viewer.
|
||||||
|
|
||||||
|
## Technology Recommendation
|
||||||
|
|
||||||
|
### Recommended stack
|
||||||
|
|
||||||
|
- Desktop shell: `Wails`
|
||||||
|
- Frontend: `React + TypeScript`
|
||||||
|
- Styling: `Tailwind CSS`
|
||||||
|
- Forms and validation: `react-hook-form + zod`
|
||||||
|
|
||||||
|
### Why Wails
|
||||||
|
|
||||||
|
`ocswitch` already centers on Go. The desktop shell should wrap that Go core rather than forcing a new primary runtime.
|
||||||
|
|
||||||
|
Wails is recommended because:
|
||||||
|
|
||||||
|
- it matches the existing Go-heavy codebase
|
||||||
|
- it avoids introducing Rust as another core runtime concern
|
||||||
|
- it supports the desktop shell product shape better than a browser-only UI
|
||||||
|
- it allows Go services to remain the real source of business behavior
|
||||||
|
|
||||||
|
### Why React over Vue
|
||||||
|
|
||||||
|
The GUI is primarily a configuration-heavy control panel.
|
||||||
|
|
||||||
|
React is recommended because:
|
||||||
|
|
||||||
|
- the form-heavy workflow aligns well with `react-hook-form + zod`
|
||||||
|
- long-term ecosystem support for control-panel style applications is strong
|
||||||
|
- it is a pragmatic default when the frontend is not the product center but must remain maintainable as scope grows
|
||||||
|
|
||||||
|
Vue is not rejected as incapable; it is simply not the preferred choice for this project's chosen form stack and expected long-term expansion.
|
||||||
|
|
||||||
|
## Current Project State
|
||||||
|
|
||||||
|
### Existing reusable layers
|
||||||
|
|
||||||
|
#### `internal/config`
|
||||||
|
|
||||||
|
Already provides:
|
||||||
|
|
||||||
|
- `Config`, `Provider`, `Alias`, `Target`, `Server` models
|
||||||
|
- config load/save
|
||||||
|
- default path resolution
|
||||||
|
- provider and alias mutation helpers
|
||||||
|
- structural validation
|
||||||
|
|
||||||
|
#### `internal/proxy`
|
||||||
|
|
||||||
|
Already provides:
|
||||||
|
|
||||||
|
- local HTTP server
|
||||||
|
- `/v1/responses`
|
||||||
|
- `/v1/models`
|
||||||
|
- deterministic alias failover behavior
|
||||||
|
- downstream streaming pass-through
|
||||||
|
|
||||||
|
#### `internal/opencode`
|
||||||
|
|
||||||
|
Already provides:
|
||||||
|
|
||||||
|
- OpenCode config load/save
|
||||||
|
- provider sync patching
|
||||||
|
- global target path resolution
|
||||||
|
|
||||||
|
#### `internal/cli`
|
||||||
|
|
||||||
|
Currently provides command entry points, but it also contains business orchestration that should move into a shared application layer.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- provider add/update orchestration
|
||||||
|
- alias bind/unbind orchestration
|
||||||
|
- doctor orchestration
|
||||||
|
- OpenCode sync orchestration
|
||||||
|
- serve command bootstrap
|
||||||
|
|
||||||
|
## Proposed Architecture
|
||||||
|
|
||||||
|
### Layer model
|
||||||
|
|
||||||
|
Recommended layers:
|
||||||
|
|
||||||
|
1. `internal/config`
|
||||||
|
2. `internal/proxy`
|
||||||
|
3. `internal/opencode`
|
||||||
|
4. `internal/app`
|
||||||
|
5. `internal/cli`
|
||||||
|
6. `internal/desktop`
|
||||||
|
7. `web/`
|
||||||
|
|
||||||
|
### Responsibility boundaries
|
||||||
|
|
||||||
|
#### `internal/config`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- persisted config structures
|
||||||
|
- file path resolution
|
||||||
|
- load/save with locking and atomic write behavior
|
||||||
|
- config-level validation helpers
|
||||||
|
|
||||||
|
Must not own:
|
||||||
|
|
||||||
|
- desktop shell behavior
|
||||||
|
- UI-specific DTOs
|
||||||
|
- tray/menu integration
|
||||||
|
|
||||||
|
#### `internal/proxy`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- proxy server construction
|
||||||
|
- request routing
|
||||||
|
- upstream retry/failover semantics
|
||||||
|
- HTTP runtime behavior
|
||||||
|
|
||||||
|
Must not own:
|
||||||
|
|
||||||
|
- desktop lifecycle decisions
|
||||||
|
- UI-specific runtime state projection
|
||||||
|
|
||||||
|
#### `internal/opencode`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- OpenCode config parsing and writing
|
||||||
|
- `provider.ocswitch` sync logic
|
||||||
|
- target resolution for the OpenCode config file
|
||||||
|
|
||||||
|
Must not own:
|
||||||
|
|
||||||
|
- GUI orchestration
|
||||||
|
- desktop notifications
|
||||||
|
|
||||||
|
#### `internal/app`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- reusable business workflows
|
||||||
|
- shared DTOs for CLI and GUI
|
||||||
|
- orchestration across `config`, `proxy`, and `opencode`
|
||||||
|
- runtime supervision of the embedded proxy process in desktop mode
|
||||||
|
|
||||||
|
Must not own:
|
||||||
|
|
||||||
|
- Wails-specific bindings
|
||||||
|
- frontend rendering
|
||||||
|
- tray/menu implementation
|
||||||
|
|
||||||
|
#### `internal/cli`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- cobra command tree
|
||||||
|
- flag parsing
|
||||||
|
- terminal printing
|
||||||
|
|
||||||
|
Must delegate business workflows to `internal/app`.
|
||||||
|
|
||||||
|
#### `internal/desktop`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- Wails application bootstrap
|
||||||
|
- window lifecycle
|
||||||
|
- tray/menu wiring
|
||||||
|
- native notifications
|
||||||
|
- launch-at-login integration
|
||||||
|
- binding frontend calls to `internal/app`
|
||||||
|
|
||||||
|
Must not own:
|
||||||
|
|
||||||
|
- provider/alias mutation rules
|
||||||
|
- OpenCode sync rules
|
||||||
|
- direct config mutation logic beyond reading desktop preference values via services
|
||||||
|
|
||||||
|
#### `web/`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- React pages and components
|
||||||
|
- visual state and form UX
|
||||||
|
- calling Wails-bound methods
|
||||||
|
|
||||||
|
Must not own:
|
||||||
|
|
||||||
|
- config file persistence rules
|
||||||
|
- routing/failover behavior
|
||||||
|
|
||||||
|
## Recommended Directory Shape
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/
|
||||||
|
ocswitch/
|
||||||
|
main.go
|
||||||
|
ocswitch-desktop/
|
||||||
|
main.go
|
||||||
|
|
||||||
|
internal/
|
||||||
|
config/
|
||||||
|
proxy/
|
||||||
|
opencode/
|
||||||
|
app/
|
||||||
|
service.go
|
||||||
|
types.go
|
||||||
|
config_service.go
|
||||||
|
provider_service.go
|
||||||
|
alias_service.go
|
||||||
|
sync_service.go
|
||||||
|
runtime_service.go
|
||||||
|
cli/
|
||||||
|
desktop/
|
||||||
|
app.go
|
||||||
|
tray.go
|
||||||
|
menu.go
|
||||||
|
notify.go
|
||||||
|
autostart.go
|
||||||
|
bindings.go
|
||||||
|
|
||||||
|
web/
|
||||||
|
package.json
|
||||||
|
src/
|
||||||
|
pages/
|
||||||
|
components/
|
||||||
|
lib/
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact filenames can vary, but the boundary itself should remain stable.
|
||||||
|
|
||||||
|
## `internal/app` Design
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
`internal/app` is the shared application service layer.
|
||||||
|
|
||||||
|
It exists to solve a current structural problem in the codebase: many workflows already exist, but they are embedded inside CLI command handlers. The desktop GUI should not duplicate those workflows, and the CLI should not remain the only caller.
|
||||||
|
|
||||||
|
`internal/app` should therefore become the only place where cross-package use cases are orchestrated.
|
||||||
|
|
||||||
|
### Design principles
|
||||||
|
|
||||||
|
1. Organize by user workflows, not by storage structs.
|
||||||
|
2. Return stable DTOs instead of exposing raw persistence objects directly to GUI code.
|
||||||
|
3. Keep validation and persistence in the existing packages where appropriate.
|
||||||
|
4. Keep the initial design small and explicit rather than overly abstract.
|
||||||
|
|
||||||
|
### Top-level service shape
|
||||||
|
|
||||||
|
Recommended root type:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Service struct {
|
||||||
|
configPath string
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
proxyCancel context.CancelFunc
|
||||||
|
proxyDone chan struct{}
|
||||||
|
proxyStatus ProxyStatusView
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(configPath string) *Service
|
||||||
|
```
|
||||||
|
|
||||||
|
The root service may either expose all methods directly or hold narrower sub-services internally. The important point is that callers should consume one coherent application boundary.
|
||||||
|
|
||||||
|
### Shared DTOs
|
||||||
|
|
||||||
|
Recommended DTOs are intentionally UI-safe and transport-safe.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Overview struct {
|
||||||
|
ConfigPath string `json:"configPath"`
|
||||||
|
ProviderCount int `json:"providerCount"`
|
||||||
|
AliasCount int `json:"aliasCount"`
|
||||||
|
AvailableAliases []string `json:"availableAliases"`
|
||||||
|
Proxy ProxyStatusView `json:"proxy"`
|
||||||
|
Desktop DesktopPrefsView `json:"desktop"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyStatusView struct {
|
||||||
|
Running bool `json:"running"`
|
||||||
|
BindAddress string `json:"bindAddress"`
|
||||||
|
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||||
|
LastError string `json:"lastError,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderView struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
APIKeySet bool `json:"apiKeySet"`
|
||||||
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
Models []string `json:"models,omitempty"`
|
||||||
|
ModelsSource string `json:"modelsSource,omitempty"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasTargetView struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasView struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Targets []AliasTargetView `json:"targets"`
|
||||||
|
Available bool `json:"available"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoctorIssue struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoctorReport struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Issues []DoctorIssue `json:"issues"`
|
||||||
|
ConfigPath string `json:"configPath"`
|
||||||
|
ProviderCount int `json:"providerCount"`
|
||||||
|
AliasCount int `json:"aliasCount"`
|
||||||
|
ProxyBindAddress string `json:"proxyBindAddress"`
|
||||||
|
OpenCodeTargetPath string `json:"openCodeTargetPath"`
|
||||||
|
OpenCodeTargetFound bool `json:"openCodeTargetFound"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncPreview struct {
|
||||||
|
TargetPath string `json:"targetPath"`
|
||||||
|
AliasNames []string `json:"aliasNames"`
|
||||||
|
SetModel string `json:"setModel,omitempty"`
|
||||||
|
SetSmallModel string `json:"setSmallModel,omitempty"`
|
||||||
|
WouldChange bool `json:"wouldChange"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncResult struct {
|
||||||
|
TargetPath string `json:"targetPath"`
|
||||||
|
AliasNames []string `json:"aliasNames"`
|
||||||
|
Changed bool `json:"changed"`
|
||||||
|
DryRun bool `json:"dryRun"`
|
||||||
|
SetModel string `json:"setModel,omitempty"`
|
||||||
|
SetSmallModel string `json:"setSmallModel,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DesktopPrefsView struct {
|
||||||
|
LaunchAtLogin bool `json:"launchAtLogin"`
|
||||||
|
MinimizeToTray bool `json:"minimizeToTray"`
|
||||||
|
Notifications bool `json:"notifications"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Input types
|
||||||
|
|
||||||
|
Recommended command inputs:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SaveProviderInput struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
APIKey string `json:"apiKey,omitempty"`
|
||||||
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
ClearHeaders bool `json:"clearHeaders"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
SkipModels bool `json:"skipModels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SaveAliasInput struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BindAliasTargetInput struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnbindAliasTargetInput struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncInput struct {
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
SetModel string `json:"setModel,omitempty"`
|
||||||
|
SetSmallModel string `json:"setSmallModel,omitempty"`
|
||||||
|
DryRun bool `json:"dryRun"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DesktopPrefsInput struct {
|
||||||
|
LaunchAtLogin bool `json:"launchAtLogin"`
|
||||||
|
MinimizeToTray bool `json:"minimizeToTray"`
|
||||||
|
Notifications bool `json:"notifications"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required service methods
|
||||||
|
|
||||||
|
At minimum, `internal/app` should expose these use cases:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *Service) GetOverview(ctx context.Context) (Overview, error)
|
||||||
|
|
||||||
|
func (s *Service) ListProviders(ctx context.Context) ([]ProviderView, error)
|
||||||
|
func (s *Service) SaveProvider(ctx context.Context, in SaveProviderInput) (ProviderView, error)
|
||||||
|
func (s *Service) EnableProvider(ctx context.Context, id string) error
|
||||||
|
func (s *Service) DisableProvider(ctx context.Context, id string) error
|
||||||
|
func (s *Service) RemoveProvider(ctx context.Context, id string) error
|
||||||
|
func (s *Service) ImportProvidersFromOpenCode(ctx context.Context, from string, overwrite bool) ([]ProviderView, error)
|
||||||
|
|
||||||
|
func (s *Service) ListAliases(ctx context.Context) ([]AliasView, error)
|
||||||
|
func (s *Service) SaveAlias(ctx context.Context, in SaveAliasInput) (AliasView, error)
|
||||||
|
func (s *Service) RemoveAlias(ctx context.Context, alias string) error
|
||||||
|
func (s *Service) BindAliasTarget(ctx context.Context, in BindAliasTargetInput) error
|
||||||
|
func (s *Service) UnbindAliasTarget(ctx context.Context, in UnbindAliasTargetInput) error
|
||||||
|
|
||||||
|
func (s *Service) RunDoctor(ctx context.Context) (DoctorReport, error)
|
||||||
|
func (s *Service) PreviewOpenCodeSync(ctx context.Context, in SyncInput) (SyncPreview, error)
|
||||||
|
func (s *Service) ApplyOpenCodeSync(ctx context.Context, in SyncInput) (SyncResult, error)
|
||||||
|
|
||||||
|
func (s *Service) StartProxy(ctx context.Context) error
|
||||||
|
func (s *Service) StopProxy(ctx context.Context) error
|
||||||
|
func (s *Service) RestartProxy(ctx context.Context) error
|
||||||
|
func (s *Service) GetProxyStatus(ctx context.Context) (ProxyStatusView, error)
|
||||||
|
|
||||||
|
func (s *Service) GetDesktopPrefs(ctx context.Context) (DesktopPrefsView, error)
|
||||||
|
func (s *Service) SaveDesktopPrefs(ctx context.Context, in DesktopPrefsInput) (DesktopPrefsView, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method behavior notes
|
||||||
|
|
||||||
|
#### `SaveProvider`
|
||||||
|
|
||||||
|
Must preserve current provider semantics already implemented in CLI orchestration:
|
||||||
|
|
||||||
|
- update-or-insert behavior
|
||||||
|
- optional model discovery
|
||||||
|
- preservation of fields not explicitly changed
|
||||||
|
- stale discovered catalog downgrade to untrusted metadata when needed
|
||||||
|
|
||||||
|
The GUI must not reimplement any of this itself.
|
||||||
|
|
||||||
|
#### `BindAliasTarget`
|
||||||
|
|
||||||
|
Must preserve current CLI semantics:
|
||||||
|
|
||||||
|
- allow combined `provider/model` parsing when provider is omitted
|
||||||
|
- validate provider existence
|
||||||
|
- validate known model when model catalog is trusted
|
||||||
|
- auto-create alias when current workflow requires it
|
||||||
|
|
||||||
|
#### `RunDoctor`
|
||||||
|
|
||||||
|
Should centralize the same checks currently wired in the CLI command:
|
||||||
|
|
||||||
|
- config validation
|
||||||
|
- OpenCode target resolution
|
||||||
|
- provider.ocswitch preview validation
|
||||||
|
|
||||||
|
#### Runtime methods
|
||||||
|
|
||||||
|
Must make desktop mode safe:
|
||||||
|
|
||||||
|
- no duplicate proxy start
|
||||||
|
- clean shutdown
|
||||||
|
- observable runtime state
|
||||||
|
- clear last-error reporting for the GUI and tray status
|
||||||
|
|
||||||
|
### Runtime supervision model
|
||||||
|
|
||||||
|
Desktop mode requires the proxy to be managed as an internal long-running component.
|
||||||
|
|
||||||
|
Recommended runtime model:
|
||||||
|
|
||||||
|
```text
|
||||||
|
desktop shell starts
|
||||||
|
-> app.Service constructed
|
||||||
|
-> user or preference requests proxy start
|
||||||
|
-> app.Service loads config and validates it
|
||||||
|
-> app.Service creates proxy.Server
|
||||||
|
-> app.Service starts ListenAndServe under managed context
|
||||||
|
-> proxy status is updated for GUI/tray visibility
|
||||||
|
```
|
||||||
|
|
||||||
|
Important implementation requirements:
|
||||||
|
|
||||||
|
- repeated start requests must be idempotent or return a clear error
|
||||||
|
- stop must call graceful shutdown through context cancellation
|
||||||
|
- runtime state must survive UI refreshes within the same process
|
||||||
|
|
||||||
|
## Desktop Shell Design
|
||||||
|
|
||||||
|
### Recommended `internal/desktop` responsibilities
|
||||||
|
|
||||||
|
#### `app.go`
|
||||||
|
|
||||||
|
- bootstrap Wails
|
||||||
|
- construct `internal/app.Service`
|
||||||
|
- wire lifecycle hooks
|
||||||
|
|
||||||
|
#### `bindings.go`
|
||||||
|
|
||||||
|
- expose thin Wails-callable methods
|
||||||
|
- forward all real work to `internal/app`
|
||||||
|
|
||||||
|
#### `tray.go`
|
||||||
|
|
||||||
|
- tray icon
|
||||||
|
- quick actions such as show window, start proxy, stop proxy, sync, quit
|
||||||
|
- tray status updates based on `app.Service` runtime state
|
||||||
|
|
||||||
|
#### `menu.go`
|
||||||
|
|
||||||
|
- native app menu entries if needed per platform
|
||||||
|
|
||||||
|
#### `notify.go`
|
||||||
|
|
||||||
|
- success and error notifications for:
|
||||||
|
- proxy start/stop
|
||||||
|
- config save failures
|
||||||
|
- sync results
|
||||||
|
|
||||||
|
#### `autostart.go`
|
||||||
|
|
||||||
|
- platform-specific launch-at-login registration
|
||||||
|
- read desired state from desktop preferences exposed through `internal/app`
|
||||||
|
|
||||||
|
## Configuration Extensions
|
||||||
|
|
||||||
|
Desktop behavior may require a small extension to the persisted config.
|
||||||
|
|
||||||
|
Recommended addition:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Desktop struct {
|
||||||
|
LaunchAtLogin bool `json:"launch_at_login,omitempty"`
|
||||||
|
MinimizeToTray bool `json:"minimize_to_tray,omitempty"`
|
||||||
|
Notifications bool `json:"notifications,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then extend `config.Config` with:
|
||||||
|
|
||||||
|
```go
|
||||||
|
Desktop Desktop `json:"desktop,omitempty"`
|
||||||
|
```
|
||||||
|
|
||||||
|
This should remain a lightweight preference store, not a place to encode platform-specific shell details.
|
||||||
|
|
||||||
|
## CLI Migration Plan
|
||||||
|
|
||||||
|
The CLI should remain as a first-class interface.
|
||||||
|
|
||||||
|
Recommended migration pattern:
|
||||||
|
|
||||||
|
1. move workflow orchestration from `internal/cli/*.go` into `internal/app`
|
||||||
|
2. keep cobra commands as thin argument adapters
|
||||||
|
3. update CLI output formatting without changing business semantics
|
||||||
|
|
||||||
|
This task is successful only if CLI and GUI both use the same business paths.
|
||||||
|
|
||||||
|
## GUI Surface Recommendation
|
||||||
|
|
||||||
|
### Initial pages
|
||||||
|
|
||||||
|
1. Overview
|
||||||
|
2. Providers
|
||||||
|
3. Aliases
|
||||||
|
4. Doctor and OpenCode Sync
|
||||||
|
5. Settings
|
||||||
|
|
||||||
|
### Overview page should show
|
||||||
|
|
||||||
|
- current config path
|
||||||
|
- proxy runtime status
|
||||||
|
- bind address
|
||||||
|
- provider count
|
||||||
|
- alias count
|
||||||
|
- last doctor result summary if available
|
||||||
|
|
||||||
|
### Provider page should support
|
||||||
|
|
||||||
|
- add provider
|
||||||
|
- edit provider
|
||||||
|
- enable/disable provider
|
||||||
|
- remove provider
|
||||||
|
- show discovered model metadata summary
|
||||||
|
|
||||||
|
### Alias page should support
|
||||||
|
|
||||||
|
- add/edit alias
|
||||||
|
- remove alias
|
||||||
|
- bind/unbind provider targets
|
||||||
|
- show failover order clearly
|
||||||
|
|
||||||
|
### Doctor and Sync page should support
|
||||||
|
|
||||||
|
- run doctor
|
||||||
|
- preview sync target
|
||||||
|
- apply sync
|
||||||
|
- show resolved OpenCode target path
|
||||||
|
|
||||||
|
### Settings page should support
|
||||||
|
|
||||||
|
- launch at login
|
||||||
|
- minimize to tray
|
||||||
|
- notifications preference
|
||||||
|
- optional auto-start proxy preference in a later iteration
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
This design task is complete when the PRD clearly defines:
|
||||||
|
|
||||||
|
1. the desktop product shape and why it is separate from the earlier local-web-viewer task
|
||||||
|
2. the recommended technology stack
|
||||||
|
3. the layer boundaries between `config`, `proxy`, `opencode`, `app`, `cli`, `desktop`, and `web`
|
||||||
|
4. the required `internal/app` service methods and DTO shapes
|
||||||
|
5. the runtime supervision model for the embedded proxy
|
||||||
|
6. the desktop-only responsibilities for Wails shell code
|
||||||
|
7. the first GUI page set and minimum feature scope
|
||||||
|
|
||||||
|
## Recommended Next Implementation Task
|
||||||
|
|
||||||
|
After this design is accepted, the next implementation task should focus on skeleton wiring only:
|
||||||
|
|
||||||
|
1. create `internal/app` with one or two end-to-end migrated workflows
|
||||||
|
2. create Wails shell bootstrap
|
||||||
|
3. wire a minimal React frontend
|
||||||
|
4. expose overview plus provider list through the new shared layer
|
||||||
|
|
||||||
|
That first implementation task should avoid trying to deliver every page and every shell integration at once.
|
||||||
83
.trellis/tasks/04-18-desktop-shell-gui/task.json
Normal file
83
.trellis/tasks/04-18-desktop-shell-gui/task.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"id": "desktop-shell-gui",
|
||||||
|
"name": "desktop-shell-gui",
|
||||||
|
"title": "Design desktop shell for ocswitch",
|
||||||
|
"description": "Define a desktop-shell architecture for ocswitch that adds launch-at-login, tray/menu, notifications, and a GUI control surface without rewriting existing Go core logic into desktop-specific code.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": "docs",
|
||||||
|
"scope": "desktop",
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "Apale",
|
||||||
|
"assignee": "Apale",
|
||||||
|
"createdAt": "2026-04-18",
|
||||||
|
"completedAt": "2026-04-18",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "master",
|
||||||
|
"worktree_path": null,
|
||||||
|
"current_phase": 6,
|
||||||
|
"next_action": [
|
||||||
|
{
|
||||||
|
"phase": 1,
|
||||||
|
"action": "brainstorm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 2,
|
||||||
|
"action": "research"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 3,
|
||||||
|
"action": "implement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 4,
|
||||||
|
"action": "check"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 5,
|
||||||
|
"action": "update-spec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 6,
|
||||||
|
"action": "record-session"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [
|
||||||
|
".trellis/tasks/04-18-desktop-shell-gui/prd.md",
|
||||||
|
"internal/config/config.go",
|
||||||
|
"internal/config/config_test.go",
|
||||||
|
"internal/app/types.go",
|
||||||
|
"internal/app/service.go",
|
||||||
|
"internal/app/service_test.go",
|
||||||
|
"internal/proxy/server.go",
|
||||||
|
"internal/opencode/opencode.go",
|
||||||
|
"internal/cli/root.go",
|
||||||
|
"internal/cli/provider.go",
|
||||||
|
"internal/cli/opencode.go",
|
||||||
|
"internal/cli/doctor.go",
|
||||||
|
"internal/cli/serve.go",
|
||||||
|
"internal/desktop/app.go",
|
||||||
|
"internal/desktop/bindings.go",
|
||||||
|
"internal/desktop/tray.go",
|
||||||
|
"internal/desktop/notify.go",
|
||||||
|
"internal/desktop/autostart.go",
|
||||||
|
"cmd/ocswitch-desktop/main.go"
|
||||||
|
],
|
||||||
|
"notes": "Implemented the first desktop-shell skeleton instead of a full Wails UI: added config-backed desktop preferences, a shared internal/app service layer, CLI reuse of shared workflows for doctor/opencode sync/provider list/serve, a minimal internal/desktop composition root, and a cmd/ocswitch-desktop bootstrap entrypoint. Verification passed with gofmt, go test ./..., and go build ./....",
|
||||||
|
"meta": {
|
||||||
|
"desktopShellRequired": true,
|
||||||
|
"recommendedShell": "wails",
|
||||||
|
"recommendedFrontend": "react",
|
||||||
|
"requiredDesktopCapabilities": [
|
||||||
|
"launch-at-login",
|
||||||
|
"tray-menu",
|
||||||
|
"native-notifications"
|
||||||
|
],
|
||||||
|
"sharedServiceLayer": "internal/app"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- @@@auto:current-status -->
|
<!-- @@@auto:current-status -->
|
||||||
- **Active File**: `journal-1.md`
|
- **Active File**: `journal-1.md`
|
||||||
- **Total Sessions**: 8
|
- **Total Sessions**: 10
|
||||||
- **Last Active**: 2026-04-18
|
- **Last Active**: 2026-04-18
|
||||||
<!-- @@@/auto:current-status -->
|
<!-- @@@/auto:current-status -->
|
||||||
|
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<!-- @@@auto:active-documents -->
|
<!-- @@@auto:active-documents -->
|
||||||
| File | Lines | Status |
|
| File | Lines | Status |
|
||||||
|------|-------|--------|
|
|------|-------|--------|
|
||||||
| `journal-1.md` | ~330 | Active |
|
| `journal-1.md` | ~420 | Active |
|
||||||
<!-- @@@/auto:active-documents -->
|
<!-- @@@/auto:active-documents -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -29,6 +29,8 @@
|
|||||||
<!-- @@@auto:session-history -->
|
<!-- @@@auto:session-history -->
|
||||||
| # | Date | Title | Commits | Branch |
|
| # | Date | Title | Commits | Branch |
|
||||||
|---|------|-------|---------|--------|
|
|---|------|-------|---------|--------|
|
||||||
|
| 10 | 2026-04-18 | Desktop shell skeleton implementation | - | `master` |
|
||||||
|
| 9 | 2026-04-18 | GUI desktop direction and architecture decision | - | `master` |
|
||||||
| 8 | 2026-04-18 | Provider discovery and model ref hardening | `09d0c1f` | `master` |
|
| 8 | 2026-04-18 | Provider discovery and model ref hardening | `09d0c1f` | `master` |
|
||||||
| 7 | 2026-04-17 | Deep code review for ocswitch | - | `master` |
|
| 7 | 2026-04-17 | Deep code review for ocswitch | - | `master` |
|
||||||
| 6 | 2026-04-17 | Rename CLI to ocswitch | `cf3dcec` | `master` |
|
| 6 | 2026-04-17 | Rename CLI to ocswitch | `cf3dcec` | `master` |
|
||||||
|
|||||||
@ -328,3 +328,93 @@ Completed provider model discovery hardening, provider/model parsing improvement
|
|||||||
### Next Steps
|
### Next Steps
|
||||||
|
|
||||||
- None - task complete
|
- None - task complete
|
||||||
|
|
||||||
|
|
||||||
|
## Session 9: GUI desktop direction and architecture decision
|
||||||
|
|
||||||
|
**Date**: 2026-04-18
|
||||||
|
**Task**: forwarding-log-viewer-gui
|
||||||
|
**Branch**: `master`
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Recorded the GUI direction change from a local web-only surface to a desktop-shell follow-up, driven by explicit requirements for launch at login, native menu/tray integration, and native notifications.
|
||||||
|
|
||||||
|
### Main Changes
|
||||||
|
|
||||||
|
- Confirmed the existing Trellis task `04-17-forwarding-log-viewer-gui` remains historically correct as a completed local web log-viewer design task and should not be retrofitted in place.
|
||||||
|
- Recorded follow-up product guidance that desktop capabilities now justify a separate native-shell track rather than a browser-only UI.
|
||||||
|
- Captured the recommended follow-up stack as `Wails + React + TypeScript + Tailwind CSS + react-hook-form + zod`.
|
||||||
|
- Captured the preferred layering so existing Go logic is not rewritten into desktop-specific code:
|
||||||
|
- `internal/config` continues config IO and validation
|
||||||
|
- `internal/proxy` continues proxy runtime and failover
|
||||||
|
- `internal/opencode` continues OpenCode sync
|
||||||
|
- new `internal/app` application service layer is shared by CLI and GUI
|
||||||
|
- Wails desktop shell owns window, tray/menu, notifications, autostart, and frontend hosting only
|
||||||
|
- Recorded the frontend recommendation that React is a better fit than Vue for this project because the desktop control panel is form-heavy and aligns better with the chosen `react-hook-form + zod` stack.
|
||||||
|
|
||||||
|
### Git Commits
|
||||||
|
|
||||||
|
(No commits - Trellis documentation update only)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [OK] No code changes; updated Trellis task metadata and workspace journal only
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
[OK] **Completed**
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Create a separate Trellis task for the desktop shell if implementation should proceed
|
||||||
|
|
||||||
|
|
||||||
|
## Session 10: Desktop shell skeleton implementation
|
||||||
|
|
||||||
|
**Date**: 2026-04-18
|
||||||
|
**Task**: Desktop shell skeleton implementation
|
||||||
|
**Branch**: `master`
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Implemented the first desktop-shell skeleton by introducing a shared internal/app layer, desktop preference persistence, CLI reuse of shared workflows, and a minimal desktop bootstrap with passing Go tests/builds.
|
||||||
|
|
||||||
|
### Main Changes
|
||||||
|
|
||||||
|
- Added `config.Desktop` to persist desktop-shell preferences (`launch_at_login`, `minimize_to_tray`, `notifications`) without coupling core runtime logic to a desktop framework.
|
||||||
|
- Added `internal/app/types.go` and `internal/app/service.go` as the shared application-service layer described in the PRD.
|
||||||
|
- Implemented shared DTOs and workflows for:
|
||||||
|
- overview
|
||||||
|
- provider listing
|
||||||
|
- doctor report generation
|
||||||
|
- OpenCode sync preview/apply plumbing
|
||||||
|
- proxy lifecycle management
|
||||||
|
- desktop preference read/write
|
||||||
|
- Refactored CLI reuse points so `doctor`, `opencode sync`, `provider list`, and `serve` now delegate through `internal/app` instead of keeping those orchestration paths CLI-only.
|
||||||
|
- Preserved CLI output behavior closely while centralizing the orchestration logic that future desktop bindings can call.
|
||||||
|
- Added `internal/desktop/` skeleton adapters (`app`, `bindings`, `tray`, `notify`, `autostart`) as placeholders for a future Wails/native shell integration, keeping desktop-only concerns outside `internal/app`.
|
||||||
|
- Added `cmd/ocswitch-desktop/main.go` as a minimal desktop bootstrap binary that exercises the shared bindings and confirms the skeleton is wired.
|
||||||
|
- Added `internal/app/service_test.go` to cover desktop preference persistence and proxy start/stop status flow.
|
||||||
|
- Verified the implementation with:
|
||||||
|
- `gofmt -w ...`
|
||||||
|
- `rtk go test ./...`
|
||||||
|
- `rtk go build ./...`
|
||||||
|
- Scope intentionally stayed at the PRD's recommended “skeleton wiring only” level: no Wails dependency, no React frontend, and no tray/autostart implementation yet.
|
||||||
|
|
||||||
|
|
||||||
|
### Git Commits
|
||||||
|
|
||||||
|
(No commits - planning session)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [OK] (Add test results)
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
[OK] **Completed**
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- None - task complete
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user