chore: record journal
This commit is contained in:
parent
8cf1a504d1
commit
29d0cabbd1
@ -54,9 +54,25 @@
|
||||
"internal/cli/serve.go",
|
||||
"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": {
|
||||
"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": [
|
||||
"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 -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 8
|
||||
- **Total Sessions**: 10
|
||||
- **Last Active**: 2026-04-18
|
||||
<!-- @@@/auto:current-status -->
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<!-- @@@auto:active-documents -->
|
||||
| File | Lines | Status |
|
||||
|------|-------|--------|
|
||||
| `journal-1.md` | ~330 | Active |
|
||||
| `journal-1.md` | ~420 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
@ -29,6 +29,8 @@
|
||||
<!-- @@@auto:session-history -->
|
||||
| # | 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` |
|
||||
| 7 | 2026-04-17 | Deep code review for ocswitch | - | `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
|
||||
|
||||
- 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