Follow up on the Menlo style ordering noted in PR #1007. Compare normal, italic and oblique explicitly after weight, retaining the existing width and name tie-breakers. The Menlo regression fails before the change with Italic, Regular, Bold Italic, Bold, and passes with Regular, Italic, Bold, Bold Italic. Group/tie-break tests, fresh Windows native discovery, focused nogui GUI/font tests, go vet and Windows/Linux nogui builds pass.
103 lines
9.2 KiB
Markdown
103 lines
9.2 KiB
Markdown
# GUI app shell
|
|
|
|
The desktop app has a tray (menu bar) icon whose click drops a compact
|
|
panel, and a regular window. Both show one small web UI served from
|
|
embedded assets. The page talks to Go over a JSON API on the same handler,
|
|
with no binding generator and no bundler. `magpie web` serves the same page
|
|
to a browser for a machine with no desktop. Package `internal/gui` hosts all
|
|
three.
|
|
|
|
## Responsibilities and sources of truth
|
|
|
|
| Part | Responsibility | Source |
|
|
| --- | --- | --- |
|
|
| Native host | `Run` starts the Wails v3 app: the tray icon, the panel, the main window, single-instance handover, `magpie://` import links and open at login. `host` implements `Windows` for the API: show and hide, open URLs and folders, copy to the clipboard, fit and tint the panel, and set the text size. | [`app.go`](../../internal/gui/app.go), [`mainwindow.go`](../../internal/gui/mainwindow.go), [`traymenu.go`](../../internal/gui/traymenu.go) |
|
|
| Backend | `startBackend` starts the gateway this process serves, unless another magpie already serves it. In that case this process only shows the other one's status (`gw` is nil). `stopServing` drains requests in flight. | [`backend.go`](../../internal/gui/backend.go) |
|
|
| API and assets | `Handler` serves `assets/` (embedded with `//go:embed assets`) and the `/api/*` routes, such as `/api/state`, `/api/set` and `/api/agents/{action}/{id}`. `held` lets one API request's reads share one catalog build (`provider.Hold`), and drops it after any write. `revalidated` and `versionedPage` name each script by its content hash, so a cache never serves an old `app.js` under a new page. | [`api.go`](../../internal/gui/api.go) |
|
|
| Browser mode | `StartWeb` serves the page and the gateway for `magpie web`. The browser signs in with a run key in a link. Gateway mode shows only Providers, Gateway, Routing, Usage, Plugins and Settings on a machine that is only a gateway. | [`web.go`](../../internal/gui/web.go), [`gatewaymode.go`](../../internal/gui/gatewaymode.go) |
|
|
| The page | `app.js` keeps one state object per view and renders it into a list, with no framework. The views have their own files: `routing.js`, `library.js`, `plugins.js` and `sessions.js`. Strings go through `t()` with translations in `i18n.js` (zh, ja and de; the English text is the key and the fallback). Dropdowns use `openProtoMenu`. Scrolling is done only by `scrollOnPurpose(e)` with the reader's event. | [`assets/app.js`](../../internal/gui/assets/app.js), [`assets/i18n.js`](../../internal/gui/assets/i18n.js) |
|
|
| No GUI build | `-tags nogui` builds magpie without the GUI: `hasGUI` is false and `runGUI` says to use `magpie tui`. The files of `internal/gui` that need Wails build under `!nogui`. | [`gui_off.go`](../../gui_off.go) |
|
|
|
|
## Runtime path
|
|
|
|
1. `magpie` with no arguments, `magpie app` or `magpie tray` calls `runGUI`, which calls `gui.Run`. A `nogui` build runs the TUI instead. A second launch hands over to the instance already running.
|
|
2. `startBackend` brings up the gateway, or finds another magpie serving it.
|
|
3. The panel and the window load `index.html` from `Handler`. `boot.js` passes the saved language, theme, text size and desktop font choices before the first paint.
|
|
4. The page reads `GET /api/state` and writes through `POST` routes. Each write answers with the new state, built under `held`.
|
|
5. Closing the window hides it. On the Mac a full-screen window first leaves full screen (`closeStep`).
|
|
6. The window opens as it was last left. Its settled size and whether it was maximised are kept in `settings.Window` and `settings.WindowMaximised` (`settle`; per machine, see `KeepOwn`). A maximised window keeps the size it restores to. `makeMain` opens it at that size (`openSize`). On its first show, `placeMain` maximises it again on the Mac and Windows (on Windows once the page has come). On Linux, `makeMain` makes it maximised with `StartState`. On Windows a size larger than the screen's work area is fitted and centred (`fitRoom`), and the larger size stays kept. The window's position is not kept.
|
|
|
|
## Constraints and failure behavior
|
|
|
|
### Desktop fonts
|
|
|
|
Settings → General offers independent interface and code fonts, each with
|
|
the installed family's actual styles. `GET /api/fonts` calls
|
|
[`fonts.List`](../../internal/fonts/fonts.go): DirectWrite on Windows,
|
|
CoreText on macOS and Fontconfig on Linux. Discovery is lazy, cached in the
|
|
handler, and refreshed by the menu. It returns names and CSS traits, never
|
|
font files or paths. A failed refresh answers 503 and preserves the previous
|
|
collection; an empty successful collection is distinct from failure.
|
|
`magpie web` and `nogui` do not expose this desktop catalogue.
|
|
|
|
`settings.UIFont` and `settings.CodeFont` keep the family, style name,
|
|
OpenType weight, CSS slope and width percentage. A missing choice keeps the
|
|
platform default. A settings POST can restore that default with an explicit
|
|
null; omitting the field preserves it, as does every browser-mode save.
|
|
`KeepOwn` preserves this computer's choices during sync and backup restore.
|
|
Invalid traits reject the save without replacing the previous settings.
|
|
|
|
[`fonts.js`](../../internal/gui/assets/fonts.js) applies choices at boot and
|
|
through `applyPrefs`. After a successful font change `host.fontsChanged`
|
|
notifies both existing webviews through `receiveFonts`, without waiting for
|
|
the other window to gain focus; a released lightweight webview uses boot
|
|
settings when recreated. An in-flight local save ignores the notification
|
|
and finishes its own queue.
|
|
The selected family precedes the existing platform fallback stack. The
|
|
interface and code traits apply independently; explicit component weights
|
|
still mark emphasis. A choice takes precedence over Omarchy's font, while
|
|
restoring the default follows the theme again. If discovery finds a saved
|
|
face missing, the page retains its name, explains the fallback and uses the
|
|
default until the face is available or the user chooses another. A discovery
|
|
error never clears a saved choice. The browser's own typography is unchanged.
|
|
|
|
The menus support search, keyboard selection and refresh. Styles are ordered
|
|
by weight, then normal, italic and oblique; width and name break remaining
|
|
ties. Family changes keep a matching style where possible, otherwise the
|
|
nearest regular face; named variable instances are offered without arbitrary
|
|
axis controls.
|
|
|
|
### Shared UI rules
|
|
|
|
- Every user-visible string has zh, ja and de translations with the same placeholders. `gui-ja.test.cjs` and `gui-de.test.cjs` fail on a missing one.
|
|
- A click never moves the page. Code scrolls a view only with the reader's event in hand (`scrollOnPurpose`); `click-scroll.test.cjs` guards this.
|
|
- There are no native `<select>` elements and no colored left-border stripes. State is shown with a dot or a swatch.
|
|
- The Usage page's cards and the tray panel's Allowances tab share one order, `settings.UsageOrder` (`byUsageOrder`). The panel's *Arrange* (`panelArrange`) moves rows in it and hides subscriptions from that tab alone (`settings.PanelUsageHidden`); both save through `POST /api/usage/arrange`, which changes only the field it is sent, and the Settings save keeps both. Hiding is display only: a menu bar cell's card shows though hidden (`panelPeek`). See `TestUsageArrangePanelHidden` and `panel-arrange.test.cjs`.
|
|
- The page must work in Chromium (Windows' WebView2) and WebKit (macOS, and WebKitGTK on Linux). GUI tests run in both engines.
|
|
- Tests never touch a live agent config. The package's `TestMain` runs under `testenv`'s home of its own. Playwright tests serve `assets/` with isolated `/api` fixtures.
|
|
- `POST /api/settings/codex-auto-review` accepts an empty value (Codex's own choice) or an exact model/group ID in `provider.Served`, including unlisted providers. A known provider with an unknown or empty model name is rejected without changing the saved reviewer or catalog tag. Generic gateway request resolution remains permissive; it is not the validator for this setting. See `Handler` in [`api.go`](../../internal/gui/api.go) and `TestCodexAutoReviewSetting` in [`codex_auto_review_test.go`](../../internal/gui/codex_auto_review_test.go).
|
|
|
|
`openModal` makes the background inert when an editor opens fresh, leaving
|
|
confirmations and menus interactive through redraws. `closeModal` releases
|
|
that background as soon as the close animation starts, so clicks reach the
|
|
page while the editor fades away. Opening an editor during that animation
|
|
protects the background again. `ux-safety.test.cjs` checks these transitions.
|
|
Routing's New group buttons share `newGroup`, which asks before replacing a
|
|
dirty group draft; Cancel retains the draft and Discard opens the new editor.
|
|
`newGroupWith` applies the same guard when creating a group from a model and
|
|
waits for `show` to accept navigation before creating its draft.
|
|
|
|
## Verification
|
|
|
|
```sh
|
|
go test -tags nogui ./internal/gui
|
|
go test -v ./internal/fonts # native discovery on each desktop OS
|
|
make test-ui # every internal/gui/tests/*.test.cjs, Chromium and WebKit
|
|
BROWSER=webkit node --test internal/gui/tests/click-scroll.test.cjs
|
|
```
|
|
|
|
`make test-ui` needs Node.js and Playwright (`playwright install chromium
|
|
webkit`). Set `NODE_PATH` when Playwright is installed outside the repo. The
|
|
Test workflow in CI does not run this suite, so a GUI change must run it
|
|
locally and report the result. See [`tests/README.md`](../../internal/gui/tests/README.md).
|