qwen-code/docs/design
ChiGao 4f084352f4
feat(cli): customize banner area (logo, title, hide) (#3710)
* docs(design): add banner customization design (#3005)

Document the design for issue #3005 (customize CLI banner area). Covers
the banner region taxonomy and what is replaceable vs. locked, the three
proposed settings (`ui.hideBanner`, `ui.customBannerTitle`,
`ui.customAsciiArt`) and their resolution pipeline, the schema additions
and wiring touch points, five alternative shapes considered, and the
security / failure-handling guards. Mirrored EN + zh-CN under
`docs/design/customize-banner-area/`. No code changes in this commit;
implementation lands in a follow-up PR.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): customize banner area (logo, title, hide)

Adds three opt-in `ui.*` settings that let users replace brand chrome on
startup while keeping the operational lines (version, auth, model, path)
locked: `hideBanner`, `customBannerTitle`, `customAsciiArt` (string,
{path}, or {small,large}).

A new resolver in `packages/cli/src/ui/utils/customBanner.ts` walks the
loaded settings, normalizes each tier per scope (so {path} resolves
against the file that declared it), reads the file with O_NOFOLLOW and a
64 KB cap on POSIX, sanitizes via a banner-specific stripper that drops
OSC/CSI/SS2/SS3 sequences while preserving newlines, and caps art at 200
lines × 200 cols and titles at 80 chars. Every soft failure logs a
`[BANNER]` warn and falls through to the bundled QWEN logo or default
brand title — banner config can never crash the CLI.

`<Header />` now picks the widest custom tier that fits via a shared
`pickAsciiArtTier` helper and falls back to `shortAsciiLogo` otherwise;
`<AppHeader />` extends the existing `showBanner` gate to honor
`hideBanner` alongside the screen-reader fallback.

Tracks #3005 and the design merged in #3671.

* docs(design): apply prettier to banner customization design

Reformats the EN and zh-CN design docs in
`docs/design/customize-banner-area/` to satisfy `npx prettier --check`:
table column alignment and trailing commas in `jsonc` examples. No
content changes — the words, tables, and code blocks all say the same
thing as before.

Carries forward the only actionable feedback from the now-closed
docs-only PR #3671, where the prettier check was the sole change
requested.

* fix(cli): address banner audit findings

Three audit-driven fixes for the banner customization feature:

1. **VSCode JSON schema accepts every documented shape.** The
   `ui.customAsciiArt` entry in
   `packages/vscode-ide-companion/schemas/settings.schema.json` was
   declared as `type: object`, which made VSCode flag the inline-string
   form (`"customAsciiArt": "  ___"`) — a shape the runtime accepts and
   the design doc recommends — as a schema violation. Replaced with a
   `oneOf` covering string, `{path}`, and `{small,large}` (with each
   tier itself string-or-`{path}`).

2. **Narrow terminals no longer leak the QWEN logo over a white-label
   deployment.** When a user supplied custom ASCII art but neither tier
   fit the terminal, `Header.tsx` previously fell back to the bundled
   `shortAsciiLogo` — silently undoing the white-label intent on small
   windows. The fallback now distinguishes "user supplied custom art"
   from "no custom art at all": in the first case the logo column is
   hidden entirely (info panel still renders); in the second case the
   default logo shows as before. Soft-failure paths (missing file,
   sanitization rejection) still fall through to `shortAsciiLogo`.

3. **Sanitizer strips C1 control bytes (0x80-0x9F).** The art and title
   strippers previously stopped at 0x7F, leaving single-byte CSI
   (`0x9B`), DCS (`0x90`), ST (`0x9C`) and other C1 controls intact —
   which legacy 8-bit terminals would still interpret. Aligned the
   ranges with the repo's existing `stripUnsafeCharacters` (in
   `textUtils.ts`) so banner content can't carry interpreted control
   bytes through.

New tests cover: C1 strip in art and title, absolute path reads,
symlink rejection on POSIX, narrow-terminal hide-on-custom-art, and
end-to-end `<AppHeader />` rendering through `resolveCustomBanner`.
The full banner suite is 48 tests (was 42).

* docs(design): clarify cross-scope tier merge and white-label fallback

Two clarifications surfaced by the audit on the implementation PR:

1. The design said `customAsciiArt` follows standard merge precedence,
   but the resolver actually walks scopes per-tier so workspace can
   override only `large` while user keeps `small`. Document that this
   per-tier walk is intentional — both because each `{path}` has to
   resolve against the file that declared it (the merged view loses
   that information) and because it lets users keep a personal default
   tier and override the other one per-workspace.

2. The render-time tier-selection step now distinguishes "user
   supplied custom art but neither tier fits" (hide the logo column
   entirely; falling back to `shortAsciiLogo` would silently undo a
   white-label deployment on narrow terminals) from "user supplied no
   custom art at all" (fall through to `shortAsciiLogo` and let the
   default-logo width gate decide). Step 5's pure soft-failure
   fallback (missing file, sanitization rejection) is unchanged —
   still `shortAsciiLogo`.

Mirrored both edits in the zh-CN translation.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(design): add size budget section to banner customization

Question raised on the implementation PR: "why is the test logo `CCA`
instead of the full `Custom Code Agent` — is there a character limit?"

There is no character-count limit on titles or art. There is a
**width budget** driven by terminal columns, plus an absolute
hard cap (200×200 art, 80-char title) to keep malformed input from
freezing layout. The existing user-facing guide didn't quantify the
budget anywhere, so users were guessing why long inline names didn't
render.

Add a "How wide can the logo be? — the size budget" subsection that
spells out the formula
(`availableLogoWidth = terminalCols − 4 − 2 − 44`), tabulates it at
80 / 100 / 120 / 200 cols, calls out that a 17-char brand like
"Custom Code Agent" can't render as a single ANSI Shadow line on most
terminals (~120 cols of art), and shows the stacked-words
`{ small, large }` recipe — including the `figlet` one-liner that
generates the corresponding `banner-large.txt`.

Mirrored in the zh-CN translation.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(design): add limits-at-a-glance table; switch demo to Custom Agent

The banner-customization design now has the size budget written down,
but the per-cap limits (80-char title, 200×200 art, 64 KB file) were
buried inside the size-budget formula table. Surface them as their own
"Limits at a glance" subsection at the top of the user-configuration
guide so users see the hard caps before they start hand-crafting art.

Also switch the running example from "Custom Code Agent" (17 chars,
~120 cols of ANSI Shadow art on one line — too wide for any common
terminal) to "Custom Agent" (12 chars, two-word stack at ~54 cols ×
12 lines, fits any terminal ≥ 104 cols). The figlet recipe is now a
two-word pipeline so a copy-paste run produces art the size the doc
claims.

Mirrored both changes in the zh-CN translation. The implementation
itself is unchanged.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address PR review + CI Lint failure

Two reviewer findings on PR #3710 (and the Lint job that fails for the
same root cause):

1. **Schema regen now reproduces the committed JSON Schema.** The CI
   Lint step runs `npm run generate:settings-schema` and fails when
   the worktree dirties — my earlier hand-authored `oneOf` got blown
   away because `customAsciiArt` is `type: 'object'` in the source
   schema and the generator had no way to emit a union.

   Add a `jsonSchemaOverride` escape-hatch field on `SettingDefinition`:
   when set, the generator emits the override verbatim (description
   carried forward) instead of the type-driven shape. Set it on
   `customAsciiArt` to express the runtime union (string | {path} |
   {small,large} where each tier is itself string-or-{path}). The
   committed schema is now regenerated from source and CI's
   regenerate-and-diff check passes; two back-to-back regens produce
   identical output.

2. **Untrusted workspace settings no longer influence the banner.**
   `collectScopedTiers()` walked `settings.workspace` directly because
   per-scope file paths are needed to resolve relative `{path}`
   entries — but that bypassed the trust gate that
   `settings.merged` enforces. An untrusted checkout could therefore
   render its own ASCII art and trigger local file reads through a
   `{path}` entry before the user trusts the folder. Skip
   `settings.workspace` entirely when `settings.isTrusted` is false.
   Two regression tests cover the gate (untrusted = workspace
   silenced, falls through to user; trusted = workspace honored).

Test suite for the banner is now 30 resolver tests + the existing
Header / AppHeader / settingsSchema tests = 66 total, all green.

* feat(cli): add ui.customBannerSubtitle for the spacer row

Adds a fourth opt-in setting to the banner customization surface.
The info panel renders four rows (title, subtitle/spacer, status,
path); the second row was a hard-coded single-space spacer up to
now. With this change a fork or white-label deployment can set
`ui.customBannerSubtitle` to a one-line subtitle (e.g. "Built-in
DataWorks Official Skills") and have it render in the secondary
text color in place of the spacer. Empty/unset preserves the
previous blank-spacer layout, so the change is back-compat.

The subtitle is sanitized through the same
`sanitizeSingleLine` helper as the title (now factored out): OSC /
CSI / SS2 / SS3 leaders dropped, every other C0/C1 control byte
replaced with a space, internal whitespace collapsed, ends
trimmed. Capped at 160 characters — looser than the title's 80
because tagline / "powered by" copy commonly runs longer — with
the same `[BANNER]` warn on truncation.

Wiring:

- `settingsSchema.ts` — new `customBannerSubtitle` entry next to
  `customBannerTitle`, `showInDialog: false` (free-form text in
  the TUI dialog isn't worth its own picker).
- `customBanner.ts` — `ResolvedBanner.subtitle` field;
  `resolveCustomBanner` populates it; `sanitizeTitle` and the new
  `sanitizeSubtitle` share the same helper.
- `Header.tsx` — when `customBannerSubtitle` is truthy the spacer
  row renders the string (secondary color, single line) instead
  of `<Text> </Text>`. Auth/model and path still sit at their
  usual positions.
- `AppHeader.tsx` — pipes `resolvedBanner.subtitle` through.
- VSCode JSON schema regenerated from source (idempotent).

Tests: 5 new resolver tests (default, sanitize, length cap,
empty, newline + C1 strip), 2 new Header tests (renders subtitle
between title and auth; spacer preserved when unset), 1 new
AppHeader integration test (end-to-end through resolver). Banner
suite is now 35 + 17 + 6 + 16 = 74 tests, all green.

Design docs (EN + zh-CN) updated: region taxonomy now lists four
B-rows; "Limits at a glance" table grows a subtitle row;
"Customization rules" matrix and "How to modify" section gain a
"Add a brand subtitle" example with a rendered four-row preview.

* docs(design): sweep stale 3-setting references after subtitle add

Self-review found several sections of the banner customization design
doc still framed for the original three settings; bring them in line
with the four-setting reality landed in c7aa4a401:

- Region taxonomy ASCII diagram now shows four B-rows
  (① title, ② subtitle, ③ status, ④ path).
- Resolution-pipeline ASCII diagram and step list pick up
  customBannerSubtitle on the input side and the title/subtitle
  sanitize step on the resolver side.
- "Settings schema additions" section lists the fourth entry,
  customBannerSubtitle, and notes the customAsciiArt
  jsonSchemaOverride that landed for VS Code schema reproducibility.
- "Wiring changes" section updates the Header prop list and the
  HeaderProps interface, replaces the brittle line-number anchors
  with file-level anchors, drops the obsolete `paths` second arg
  from resolveCustomBanner, and adds the trust-gate sentence.
- "Security & failure handling" table replaces the
  stripTerminalControlSequences shorthand with the actual
  banner-specific stripper, splits the title/subtitle row to cover
  both, and adds the untrusted-workspace gate as its own row.
- "Verification plan" gains two scenarios: the subtitle row, and
  the untrusted-workspace check that the Critical reviewer comment
  on the impl PR explicitly asked us to lock down.

Mirrored every edit in the zh-CN translation. The implementation
itself is unchanged.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address banner re-review (FIFO, mutex schema, display width, regex dedupe)

Addresses the five findings on PR #3710 from the latest re-review:

1. **[Critical] FIFO/pipe at `customAsciiArt.path` no longer hangs
   startup.** The resolver was calling `openSync(path, O_NOFOLLOW)`
   *before* the `fstatSync(...).isFile()` check; on POSIX, opening a
   FIFO read-only blocks until a writer connects, and `O_NOFOLLOW`
   doesn't help — it only refuses symlinks at the final path
   component. `readArtFile` now `lstatSync()`s first and refuses
   non-regular files (FIFO / socket / device / symlink) before the
   open, while keeping the post-open `fstatSync` check for TOCTOU
   safety against a swap between the lstat and the open. New
   POSIX-only regression test `mkfifo`s a named pipe and asserts the
   resolver soft-fails inside 1 s; if the open ever regresses to
   blocking, the test will hang past the timeout and the assertion
   will catch it.

2. **[Suggestion] `{path}` and `{small,large}` are now mutually
   exclusive in both schema and runtime.** The `jsonSchemaOverride`
   on `ui.customAsciiArt` is split into three branches (string,
   `{path}`, `{small?, large?}`); none of them allow `path` and tier
   keys to co-exist. `normalizeTiers()` mirrors that — an object
   carrying both kinds of keys is now soft-rejected with a `[BANNER]`
   warn rather than letting `path` silently win and dropping the
   tier values. New regression test pins the runtime side.

3. **[Suggestion] Column cap and tier-fit selection now measure in
   terminal cells.** `getAsciiArtWidth` (in `textUtils.ts`) and the
   `MAX_ART_COLS` cap in `customBanner.ts` were both using UTF-16
   `.length`, so 200 CJK fullwidth characters would slip the cap and
   render at ~400 cells, and `pickAsciiArtTier`'s width-fit check
   was wrong for any non-ASCII art. Switched both to
   `getCachedStringWidth` (string-width semantics, already in the
   repo); art truncation walks code points until adding another
   would push the cell width past the cap, so we never split a
   fullwidth code point or surrogate pair down the middle. New
   regression test exercises the CJK fullwidth case.

4. **[Suggestion] `collectScopedTiers()` no longer drops a whole
   scope just because it has no `file.path`.** Inline-string tiers
   don't need an owning settings directory; only `{path}` tiers do.
   The path-presence check was moved into the `{path}` branch, so a
   path-less scope (e.g. `systemDefaults`, future SDK-injected
   scopes) can still contribute inline art. `{path}` entries in such
   a scope soft-fail with a tier-specific `[BANNER]` warn rather
   than killing the whole scope. Two regression tests cover both
   sides.

5. **[Suggestion] OSC / CSI / SS2-3 regex are now authored once.**
   Extracted `TERMINAL_OSC_REGEX`, `TERMINAL_CSI_REGEX`,
   `TERMINAL_SHIFT_DCS_REGEX` from `stripTerminalControlSequences`
   in `@qwen-code/qwen-code-core` and re-export them from the
   package index. `customBanner.ts` reuses the constants for
   `sanitizeArt` (which still has to preserve `\n` / `\t`) and
   delegates the title/subtitle pipeline directly to
   `stripTerminalControlSequences`. Also backported the C1 control
   strip (0x80-0x9F) into the core helper so all callers
   (session-title, etc.) benefit from the same coverage; banner
   sanitizer was the only place catching single-byte CSI / DCS / ST.

Banner suite is now 40 + 17 + 6 + 16 = 79 tests, all green. Schema
regen is still byte-for-byte idempotent. `npm run typecheck` and
prettier clean on touched files.

* fix(cli): replace require() with ES6 import in FIFO test (lint)

The FIFO regression test in 7ccbfaeb1 used a synchronous `require()` to
pull in `node:child_process` so the test could lazy-load `execFileSync`
only when needed. CI Lint flagged it under `no-restricted-syntax` —
the repo enforces ES6 imports throughout, including in tests, with no
exception for `require()`.

Move the import to the top of the file alongside the other `node:` /
vitest imports. The `try/catch` around `execFileSync('mkfifo', ...)`
still gates the test on `mkfifo` being available (rare on a fresh
container, so we skip rather than fail). 40 / 40 tests still pass and
ESLint is clean on the touched file.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-07 10:17:53 +08:00
..
adaptive-output-token-escalation fix(core): recover from truncated tool calls via multi-turn continuation (#3313) 2026-04-21 17:04:24 +08:00
auto-memory feat(memory): managed auto-memory and auto-dream system (#3087) 2026-04-16 20:05:45 +08:00
channels docs(channels): consolidate design docs into single file 2026-04-02 11:17:37 +08:00
compact-mode feat: optimize compact mode UX — shortcuts, settings sync, and safety (#3100) 2026-04-16 09:29:24 +08:00
customize-banner-area feat(cli): customize banner area (logo, title, hide) (#3710) 2026-05-07 10:17:53 +08:00
fork-subagent feat(core): implement fork subagent for context sharing (#2936) 2026-04-14 14:27:38 +08:00
prompt-suggestion fix(followup): prevent tool call UI leak and Enter accept buffer race (#2872) 2026-04-09 00:07:03 +08:00
session-recap fix(cli): rework session recap rendering and add blur threshold setting (#3482) 2026-04-21 14:39:13 +08:00
session-title feat(session): auto-title sessions via fast model, add /rename --auto (#3540) 2026-04-23 20:37:05 +08:00
slash-command feat(cli): Phase 2 — slash command multi-mode expansion, ACP fixes, and UX improvements (#3377) 2026-04-22 19:12:44 +08:00
tool-use-summary fix(cli): add API Key option to qwen auth interactive menu (#3624) 2026-04-27 22:01:47 +08:00
openrouter-auth-and-models.md Feat/openrouter auth (#3576) 2026-04-27 14:47:44 +08:00