Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 463 additions & 0 deletions docs/design/web-shell-brand-customization.md

Large diffs are not rendered by default.

19 changes: 13 additions & 6 deletions docs/developers/daemon-ui/sidebar-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,19 @@ interface WebShellSidebarBranding {
}
```

| Value | Effect |
| -------------------------------- | ------------------------------------------------- |
| `undefined` (default) | Qwen logo + "Qwen Code" text |
| `false` | Branding row hidden entirely |
| `{ render: () => <MyHeader /> }` | Full replacement with custom content |
| `{ hideWhenCompact: false }` | Keep branding visible in collapsed icon-rail mode |
| Value | Effect |
| -------------------------------- | ------------------------------------------------------------------------------------------ |
| `undefined` (default) | Resolved brand: `brand` prop → daemon `GET /brand` → built-in Qwen logo + "Qwen Code" text |
| `false` | Branding row hidden entirely |
| `{ render: () => <MyHeader /> }` | Full replacement with custom content |
| `{ hideWhenCompact: false }` | Keep branding visible in collapsed icon-rail mode |

The default row is data-driven, not fixed: a daemon that serves a `ui.brand`
configuration renames the text and swaps the mark, and an embedding host can
override both with the shell component's `brand` prop (`onBrandResolved` reports
the outcome for the host's own chrome). `branding.render` stays the
highest-precedence override — it wins over the prop and the daemon-resolved
value, exactly as before.

```tsx
sidebar={{
Expand Down
21 changes: 21 additions & 0 deletions docs/developers/qwen-serve-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,27 @@ Stable contract: when `v` increments the frame layout has changed in a backwards

The workspace feature tags and `workspaces[]` are dynamic. Clients that add a workspace must fetch `/capabilities` again after the mutation completes; the daemon does not broadcast capability changes to clients that cached an earlier response. Forgetting persistence does not unload an active runtime, so that runtime remains advertised until restart.

### `GET /brand`

The Web Shell's product branding, for hosts that want to white-label the shell. Resolved from `ui.brand` in the operator settings scopes. Gated by the `web_shell_brand` feature tag; a daemon without the route answers 404.

```json
{
"name": "QiuQiu Code",
"logoDataUri": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E"
}
```

> **Both fields are optional, and `{}` is a normal response.** An absent field means "use the client's built-in brand" — the Web Shell renders its own name and inline logo. Clients must not treat an empty body as an error.

> **The handler always answers 200, even when the configured logo was rejected.** A missing file, a symlink, a hard-linked file, a directory, a non-SVG document, or content over 32 KiB yields a body with no `logoDataUri`, and the reason is written to the daemon's stderr as `qwen serve: GET /brand: ui.brand.logoPath …`. A client therefore cannot distinguish "no brand configured" from "the operator's logo was refused"; the operator-facing channel is the daemon log. One advisory is softer than a rejection, and there are two: a root `<svg>` with no usable `viewBox` and no positive, non-percentage `width`/`height` (a malformed or zero-area viewBox counts as unusable), which the browser may render blank at the sidebar's fixed size; and a prefix-bound root whose unprefixed elements lack a default-namespace binding, which renders them invisible. Middleware in front of the handler answers before it ever runs — `401` when bearer auth is required and absent, `429` when the optional rate limiter is engaged. A draining daemon does not reject this route: the rate limiter is permissive while draining, so the handler keeps answering 200 until the listener closes and the client sees a connection failure instead. Startup is not a 503 on this route: on the default deferred-runtime path the request is _held_ until the runtime is ready and then answered 200, so a short client timeout is what can fire early. A 503 with `code: "daemon_runtime_starting"` reaches this route only on configurations that answer before the runtime is ready (e.g. `--open`) and is retryable; a 503 with `code: "daemon_runtime_failed"` is terminal until the daemon restarts and carries no `Retry-After` — do not retry it.

> **Workspace settings never contribute.** The route loads settings with `skipWorkspaceSettings`, so a repository's `.qwen/settings.json` cannot rename the product or name a file for the daemon to read and inline into every connected browser. Only System Defaults, User and System are read, in that precedence. For the same reason, a brand value is refused with a warning on the daemon's stderr whenever placeholder substitution would change it: substitution draws from the process-wide environment, which a workspace's `.qwen/.env` or `env` block populates first at boot. An unresolvable placeholder (the variable is unset) is kept verbatim, so a typo'd variable shows as literal text rather than silently falling back.

> **`logoDataUri` must be rendered as an image, never injected as markup.** The daemon does not sanitize the SVG it read. SVG loaded through an `img` src or a favicon href cannot execute script; SVG injected into the document can. The Web Shell only ever assigns it to an image context, and that invariant is what makes the absence of a sanitizer safe.

> **Process-global.** The route takes no workspace selector and no session id: the value derives from user-global configuration, so it is the same for every workspace the daemon serves. It is registered after `bearerAuth` and the rate limiter, and before the Web Shell SPA fallback, so it answers JSON for any `Accept` header.

### `POST /workspaces`

Register an additional workspace runtime. The path must be an existing, accessible, absolute directory that does not duplicate or nest with another registered workspace. Registration is process-local unless the client sends `persist: true`; clients must pre-flight `persistent_workspace_registration` before requesting persistence. When `workspace_display_name` is advertised, the request may also include an optional `displayName`.
Expand Down
2 changes: 2 additions & 0 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ These settings are read from operator scopes only (User, System, and SystemDefau
| --------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `ui.theme` | string | The color theme for the UI. See [Themes](../configuration/themes) for available options. | `"Qwen Dark"` |
| `ui.customThemes` | object | Custom theme definitions. | `{}` |
| `ui.brand.name` | string | Product name the Web Shell presents in its sidebar, welcome header, About panel and browser tab title. Sanitized to a single line and capped at 80 characters. Read from operator scopes only (User, System, SystemDefaults) — a workspace `.qwen/settings.json` cannot rebrand the shell. A placeholder value that substitution would change (`$VAR`/`${VAR}` with the variable set) is refused with a warning on the daemon's stderr, because the substitution source is process-wide and a workspace could supply it; an unresolvable placeholder is kept verbatim, so a typo'd variable shows as literal text rather than silently falling back. Not editable from the in-browser Settings page. The terminal banner has its own separate setting, `ui.customBannerTitle`. | `""` |
| `ui.brand.logoPath` | string | Path to an SVG used as the Web Shell sidebar logo and browser favicon. A leading `~` is expanded, and a relative path resolves against the directory of the settings file that declares it. Must be a regular file (not a symlink, and not reachable through more than one hard link) of at most 32 KiB — on disk and once UTF-8-decoded — whose root element is a namespaced `<svg>` (the `xmlns` attribute, or an `xmlns:svg` binding on a prefix-bound root, is what makes it renderable as an image). Environment variable placeholders that would resolve are refused, as with `ui.brand.name`. Read from operator scopes only. A rejected file logs a warning on the daemon's stderr and falls back to the built-in logo. A root `<svg>` with no usable `viewBox` or explicit positive width/height is accepted but logs an advisory, and so is a prefix-bound root whose unprefixed elements have no default-namespace binding, because the browser may render it blank at the sidebar's fixed size. | `""` |
| `ui.statusLine` | object | Custom status line configuration. Supports `command`, `refreshInterval`, `respectUserColors`, and `hideContextIndicator` options. See [Status Line](../features/status-line). | `undefined` |
| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` |
| `ui.hideTips` | boolean | Hide all tips (startup and post-response) in the UI. See [Contextual Tips](../features/tips). | `false` |
Expand Down
1 change: 1 addition & 0 deletions integration-tests/cli/qwen-serve-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ describe('qwen serve — capabilities envelope', () => {
'extension_batch_activation_v2',
'extension_activation_explicit_refresh',
'workspace_skill_manage',
'web_shell_brand',
'workspace_settings',
'workspace_permissions',
'workspace_voice',
Expand Down
Loading
Loading