feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)
* feat(desktop): add native host scaffold * feat(webui): track turns and usage in gateway * feat(webui): polish desktop chat experience * feat(apps): add ArcGIS and Joplin logos * feat(desktop): polish shell and shared surfaces * fix(webui): avoid preview chips for glob references * test: align CI expectations for token fallback * feat(webui): preview prompt rail entries * feat(webui): add prompt navigator drawer * style(webui): refine prompt navigator placement * style(webui): align prompt navigator with header actions * style(webui): simplify prompt navigator header * refactor(webui): clean thread resource refresh * feat(desktop): add native reply notifications * fix(webui): preserve desktop restart and replay state * fix(desktop): harden gateway proxy startup * fix(web): fall back when readability is unavailable * fix(desktop): hide window instead of closing on macos * fix(webui): unify desktop header actions * fix(webui): simplify prompt history rows * fix(desktop): log notification delivery failures * chore(desktop): clean source package artifacts * fix(cron): support one-time relative reminders * fix(webui): reveal scroll button in place * Revert "fix(cron): support one-time relative reminders" This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b. * refactor(webui): extract token usage heatmap * docs(desktop): clarify contributor guides --------- Co-authored-by: chengyongru <2755839590@qq.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# Desktop Development Guide
|
||||
|
||||
This guide is for GitHub contributors who want to change the desktop app. If
|
||||
you are using nanobot rather than developing it, the important bit is simpler:
|
||||
desktop runs the local engine for you and shows the same chat, settings, apps,
|
||||
skills, and workspace UI as the browser WebUI.
|
||||
|
||||
`desktop` is the native host for the shared nanobot WebUI. It is not a fork of
|
||||
the WebUI, and it should not grow a second copy of product UI.
|
||||
|
||||
The healthy mental model is:
|
||||
|
||||
```text
|
||||
nanobot core -> agent runtime, gateway, providers, tools, memory
|
||||
webui -> shared product UI and runtime-aware UI
|
||||
desktop -> native host, engine lifecycle, secure host capabilities
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Use this when developing from a source checkout.
|
||||
|
||||
Run the shared WebUI dev server:
|
||||
|
||||
```sh
|
||||
cd desktop
|
||||
bun run dev:webui
|
||||
```
|
||||
|
||||
Run the Electron host in another terminal:
|
||||
|
||||
```sh
|
||||
cd desktop
|
||||
bun run dev:app
|
||||
```
|
||||
|
||||
In development, Electron loads `http://127.0.0.1:5173`, so changes under
|
||||
`webui/src` hot reload. Changes under `desktop/src` require restarting
|
||||
`dev:app`.
|
||||
|
||||
For source checkouts, the host starts the engine with local `python3` and
|
||||
injects the repository root into `PYTHONPATH`. This means Python changes under
|
||||
`nanobot/` are picked up from the current checkout.
|
||||
|
||||
## Where Code Goes
|
||||
|
||||
Use this table before adding a desktop feature:
|
||||
|
||||
| Change | Location |
|
||||
| --- | --- |
|
||||
| Agent behavior, tools, providers, memory, config schema | `nanobot/` |
|
||||
| Shared chat UI, settings UI, reusable product UI | `webui/` |
|
||||
| Runtime-aware UI rows, such as native engine status or open logs buttons | `webui/` |
|
||||
| The implementation behind native capabilities | `desktop/src/main.ts` |
|
||||
| The trusted renderer bridge contract | `desktop/src/preload.cts` and `desktop/docs/host-contract.md` |
|
||||
| Electron window, app protocol, native menus, lifecycle, packaging | `desktop/src` and `desktop/package.json` |
|
||||
| WebSocket-over-Unix-socket bridge | `desktop/src/unixWebSocket.ts` |
|
||||
| Bundled Python runtime preparation | `desktop/scripts/prepare-engine.mjs` |
|
||||
|
||||
For example, if desktop Settings needs an "Open logs" button, the button belongs
|
||||
in the shared WebUI settings page because it is product UI. The actual filesystem
|
||||
operation belongs in the desktop host and is exposed through `window.nanobotHost`.
|
||||
|
||||
## Host Contract
|
||||
|
||||
The shared WebUI talks to desktop through `window.nanobotHost`. WebUI code may
|
||||
check for host capabilities, but it must not import Electron, Node.js modules,
|
||||
or desktop source files.
|
||||
|
||||
Prefer capability-driven UI:
|
||||
|
||||
```text
|
||||
if host can open logs -> show Open logs
|
||||
if host can restart engine -> show Restart engine
|
||||
```
|
||||
|
||||
Avoid platform-driven UI:
|
||||
|
||||
```text
|
||||
if desktop -> run Electron-specific logic in WebUI
|
||||
```
|
||||
|
||||
This keeps the WebUI usable in browsers and leaves room for future native hosts
|
||||
without rewriting product screens.
|
||||
|
||||
## Adding A Desktop Feature
|
||||
|
||||
Before implementing, answer these questions:
|
||||
|
||||
1. Is this product UI or a native capability?
|
||||
2. Can the WebUI express it through a generic capability instead of a desktop flag?
|
||||
3. Does the host API validate trusted origins and accepted URL schemes?
|
||||
4. Does browser WebUI still work when `window.nanobotHost` is missing?
|
||||
5. Does the engine behavior belong in nanobot core instead of Electron?
|
||||
6. Does packaged mode use app data for user state instead of app resources?
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Do not copy or fork `webui/src` into `desktop/`.
|
||||
- Do not import Electron or Node.js modules from `webui/src`.
|
||||
- Do not add provider-specific onboarding screens to `desktop/`.
|
||||
- Do not duplicate WebUI settings or login flows in Electron-owned HTML.
|
||||
- Do not make `desktop/src/main.ts` own agent behavior.
|
||||
- Do not commit `desktop/node_modules`, `desktop/build`, `desktop/dist`, DMGs,
|
||||
or `desktop/resources/nanobot-engine`.
|
||||
|
||||
## Release Shape
|
||||
|
||||
Release builds assemble three existing parts:
|
||||
|
||||
1. the shared WebUI build from `nanobot/web/dist`,
|
||||
2. the Python engine prepared under `desktop/resources/nanobot-engine`,
|
||||
3. the Electron host compiled from `desktop/src`.
|
||||
|
||||
User config, logs, sessions, workspace state, and the default workspace live in
|
||||
the platform app data directory, not inside the app bundle.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Native Host Contract
|
||||
|
||||
This is a contributor reference for the boundary between the shared WebUI and
|
||||
the native desktop host. Users should not need this contract to run the app, but
|
||||
it explains why the desktop app can use native capabilities without turning the
|
||||
WebUI into Electron-specific code.
|
||||
|
||||
`desktop` is a native host shell around the shared WebUI build. The renderer
|
||||
must not import Electron directly. It receives a minimal bridge at
|
||||
`window.nanobotHost`.
|
||||
|
||||
## Runtime API
|
||||
|
||||
```ts
|
||||
type HostRuntimeInfo = {
|
||||
surface: "native";
|
||||
app_version: string;
|
||||
engine_status: "starting" | "ready" | "restarting" | "stopped" | "crashed";
|
||||
data_dir: string;
|
||||
logs_dir: string;
|
||||
config_path: string;
|
||||
workspace_path: string;
|
||||
python: string;
|
||||
engine_transport?: "unix_socket";
|
||||
};
|
||||
|
||||
type HostSocketEvent =
|
||||
| { id: string; type: "open" }
|
||||
| { id: string; type: "message"; data: string }
|
||||
| { id: string; type: "error"; message: string }
|
||||
| { id: string; type: "close"; code?: number; reason?: string };
|
||||
|
||||
type NanobotHost = {
|
||||
getRuntimeInfo(): Promise<HostRuntimeInfo>;
|
||||
restartEngine(): Promise<void>;
|
||||
pickFolder(): Promise<string | null>;
|
||||
openLogs(): Promise<void>;
|
||||
exportDiagnostics(): Promise<string>;
|
||||
checkForUpdates(): Promise<{ supported: boolean; message?: string }>;
|
||||
openSocket(url: string): Promise<string>;
|
||||
sendSocket(id: string, data: string): Promise<void>;
|
||||
closeSocket(id: string): Promise<void>;
|
||||
onSocketEvent(listener: (event: HostSocketEvent) => void): () => void;
|
||||
onRuntimeStatus(listener: (status: HostRuntimeInfo["engine_status"]) => void): () => void;
|
||||
};
|
||||
```
|
||||
|
||||
## First Run
|
||||
|
||||
The desktop host starts the private engine immediately. If the native data
|
||||
directory has no `config.json`, `nanobot desktop-gateway` creates one with
|
||||
defaults before serving the shared WebUI. Provider, model, credential, and login
|
||||
setup stay in WebUI settings instead of Electron-owned HTML.
|
||||
|
||||
## Socket Bridge
|
||||
|
||||
The engine listens on a per-user Unix socket under the app data directory.
|
||||
`/webui/bootstrap` returns `runtime_surface: "native"` and a WebSocket URL in
|
||||
the `nanobot-host://engine/...` scheme. WebUI never opens that URL directly in
|
||||
the browser runtime; it hands the URL to `window.nanobotHost.openSocket`.
|
||||
|
||||
The native host then performs the WebSocket handshake against the Unix socket
|
||||
and forwards events over Electron IPC.
|
||||
|
||||
## Host Security Boundary
|
||||
|
||||
The host bridge is intentionally narrower than a general Electron preload:
|
||||
|
||||
- IPC calls are accepted only from renderer frames loaded from `nanobot-app://app/...`.
|
||||
- `openSocket` accepts only `nanobot-host://engine/...` URLs.
|
||||
- External navigation is denied in the app window; safe web links are opened by
|
||||
the operating system.
|
||||
- Native WebUI responses carry a restrictive Content Security Policy and
|
||||
`X-Content-Type-Options: nosniff`.
|
||||
- The renderer runs with `nodeIntegration: false`, `contextIsolation: true`,
|
||||
`sandbox: true`, and `webSecurity: true`.
|
||||
|
||||
Security-sensitive tool behavior still belongs in nanobot core. The host
|
||||
protects the native app boundary; the engine protects file, network, and tool
|
||||
permissions.
|
||||
|
||||
## Data Directory
|
||||
|
||||
The host stores config, workspace, sessions, logs, and transient socket files
|
||||
under Electron's platform app data directory. In development on macOS this is
|
||||
usually:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/@nanobot/desktop/
|
||||
```
|
||||
|
||||
Packaged builds use the packaged app name.
|
||||
|
||||
The app bundle is replaceable. User data is not stored in the bundle.
|
||||
@@ -0,0 +1,71 @@
|
||||
# WebUI Sync Workflow
|
||||
|
||||
This workflow is for contributors keeping the desktop app and browser WebUI in
|
||||
sync. Users should experience them as one product surface: desktop adds a native
|
||||
host and local engine lifecycle, while chat, settings, apps, skills, and
|
||||
workspace UI still come from the shared WebUI.
|
||||
|
||||
`desktop` consumes the shared WebUI build output. It must not copy, fork, or
|
||||
vendor `webui/src`.
|
||||
|
||||
## Development
|
||||
|
||||
Run the WebUI dev server:
|
||||
|
||||
```sh
|
||||
cd desktop
|
||||
bun run dev:webui
|
||||
```
|
||||
|
||||
Run the native host in another terminal:
|
||||
|
||||
```sh
|
||||
cd desktop
|
||||
bun run dev:app
|
||||
```
|
||||
|
||||
The host loads `http://127.0.0.1:5173` in development, so React changes hot
|
||||
reload. Main/preload changes still require restarting `dev:app`.
|
||||
|
||||
## Release Build
|
||||
|
||||
1. Build the shared WebUI:
|
||||
|
||||
```sh
|
||||
bun run build --prefix webui
|
||||
```
|
||||
|
||||
2. Prepare the bundled Python engine:
|
||||
|
||||
```sh
|
||||
cd desktop
|
||||
NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine
|
||||
```
|
||||
|
||||
3. Build the app:
|
||||
|
||||
```sh
|
||||
bun run make:mac:arm64
|
||||
bun run make:mac:x64
|
||||
```
|
||||
|
||||
`electron-builder` packages `nanobot/web/dist` as `Resources/nanobot-webui`.
|
||||
|
||||
## Checklist
|
||||
|
||||
- WebUI source remains host-neutral: it may branch on generic runtime
|
||||
capabilities, but it must not import Electron or desktop source files.
|
||||
|
||||
```sh
|
||||
rg -n "from ['\\\"]electron|desktop/src|nanobotDesktop" webui/src
|
||||
```
|
||||
|
||||
This command should print nothing.
|
||||
|
||||
- Native host behavior is implemented in `desktop/src`.
|
||||
- Provider, model, credential, and login setup stay in shared WebUI settings.
|
||||
Do not duplicate those flows in Electron-owned HTML.
|
||||
- Shared UI behavior is implemented in `webui/src` through `window.nanobotHost`
|
||||
and generic runtime capability checks.
|
||||
- Do not copy React components from `webui/src` into this folder.
|
||||
- Do not commit bundled runtimes, DMGs, or `node_modules`.
|
||||
Reference in New Issue
Block a user