Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
374 changes: 374 additions & 0 deletions docs/design/web-shell-brand-customization.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/developers/daemon/00-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Use these anchors when moving from the docs into the latest `main` code:
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| HTTP routes | The route catalog lives in `qwen-serve-protocol.md`; this daemon set only references it and explains implementation ownership. | [`../qwen-serve-protocol.md`](../qwen-serve-protocol.md), [`20`](./20-quickstart-operations.md) |
| Event schema | `EVENT_SCHEMA_VERSION = 1`; 53 known event types; id-less subscriber synthetic frames; `_meta.serverTimestamp` stamped by `EventBus.publish()` (with `formatSseFrame()` fallback for synthetic frames). | [`09`](./09-event-schema.md), [`10`](./10-event-bus.md) |
| Capabilities | `SERVE_PROTOCOL_VERSION = 'v1'`; 157 registered tags; 45 conditional tags. | [`11`](./11-capabilities-versioning.md) |
| Capabilities | `SERVE_PROTOCOL_VERSION = 'v1'`; 158 registered tags; 45 conditional tags. | [`11`](./11-capabilities-versioning.md) |
| Session shell | `POST /session/:id/shell` exists behind `--enable-session-shell`, bearer or trusted-loopback authority, and session-bound `X-Qwen-Client-Id`; capability tag is conditional. | [`11`](./11-capabilities-versioning.md), [`17`](./17-configuration.md), [`20`](./20-quickstart-operations.md) |
| Rate limiting | Optional per-tier HTTP rate limit is exposed by CLI flags/env and conditional capability tag. | [`11`](./11-capabilities-versioning.md), [`17`](./17-configuration.md) |

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 @@ -1137,6 +1137,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.

> **Always 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.
Comment thread
wenshao marked this conversation as resolved.
Resolved

> **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.

> **`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. 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) of at most 32 KiB whose root element is `<svg>`. Read from operator scopes only. A rejected file logs a warning on the daemon's stderr and falls back to the built-in logo. | `""` |
| `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 @@ -424,6 +424,7 @@ describe('qwen serve — capabilities envelope', () => {
'workspace_skill_settings_batch_toggle',
'extension_batch_activation_v2',
'workspace_skill_manage',
'web_shell_brand',
'workspace_settings',
'workspace_permissions',
'workspace_voice',
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,24 @@ describe('SettingsSchema', () => {
).toBe(false);
});

it('should define the web shell brand as deployment-only configuration', () => {
const brand = getSettingsSchema().ui.properties.brand;

expect(brand.type).toBe('object');
// Edited in settings.json, not from the in-browser Settings page: brand
// is deployment identity, not a preference one viewer of a workspace
// should be able to change for everyone else.
expect(brand.showInDialog).toBe(false);

const { name, logoPath } = brand.properties;
expect(name.type).toBe('string');
expect(name.default).toBe('');
expect(name.showInDialog).toBe(false);
expect(logoPath.type).toBe('string');
expect(logoPath.default).toBe('');
expect(logoPath.showInDialog).toBe(false);
});

it('should define Markdown render mode as a user-facing UI enum', () => {
const renderMode = getSettingsSchema().ui.properties.renderMode;

Expand Down
32 changes: 32 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,38 @@ const SETTINGS_SCHEMA = {
description: 'The color theme for the UI.',
showInDialog: true,
},
brand: {
type: 'object',
label: 'Web Shell Brand',
category: 'UI',
requiresRestart: false,
default: {},
description:
'Product name and logo the Web Shell presents. Read from operator scopes only (System Defaults, User, System) — a workspace settings file cannot rebrand the shell, because it commonly comes from a repository the person opening the shell did not write. Not editable from the in-browser Settings page; edit settings.json directly.',
showInDialog: false,
properties: {
name: {
type: 'string',
label: 'Web Shell Brand Name',
category: 'UI',
requiresRestart: false,
default: '' as string,
description:
'Product name shown in the Web Shell sidebar, welcome header, About panel and browser tab title. Sanitized to a single line; capped at 80 characters. Leave empty to use the built-in name. The terminal banner has its own separate setting, `ui.customBannerTitle`.',
showInDialog: false,
},
logoPath: {
type: 'string',
label: 'Web Shell Brand Logo Path',
category: 'UI',
requiresRestart: false,
default: '' as string,
description:
'Path to an SVG file 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. The file must be a regular file, not a symlink, and at most 32 KiB. Leave empty to use the built-in logo.',
showInDialog: false,
},
},
},
autoModeAcknowledged: {
type: 'boolean',
label: 'Auto Mode Acknowledged',
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/serve/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ export const SERVE_CAPABILITY_REGISTRY = {
workspace_skill_settings_batch_toggle: { since: 'v1' },
extension_batch_activation_v2: { since: 'v1' },
workspace_skill_manage: { since: 'v1' },
// `GET /brand` — the Web Shell's product name and logo, resolved from the
// operator settings scopes. Unconditional because the route is registered
// unconditionally. Advertised so a host can preflight rather than issue the
// request and swallow a 404 from a daemon too old to have it.
web_shell_brand: { since: 'v1' },
workspace_settings: { since: 'v1' },
// `GET /workspace/permissions` is always available when this tag is
// advertised. `POST /workspace/permissions` updates the active ACP
Expand Down
Loading
Loading