Skip to Content
ДизайнCustomize Banner AreaBanner 自定义区域设计方案

Banner 自定义区域设计方案

允许用户替换 QWEN ASCII Logo、替换品牌标题、整体隐藏 Banner —— 但不允许抹掉用于排障与可信度的运行时信息(版本号、鉴权方式、模型、 工作目录)。

概述

Qwen Code CLI 启动时会在终端顶部打印一个 Banner,包含 QWEN ASCII Logo 和一个带边框的信息面板。多种真实场景需要对这一区域进行控制:

  • 白标 / 第三方品牌集成:将 Qwen Code 嵌入企业或团队自有产品时, 需要展示自家品牌而非默认的 “Qwen Code”。
  • 个性化:个人用户希望让终端 Banner 与团队规范或个人审美一致。
  • 多租户 / 多实例区分:在共享环境下,不同团队希望快速辨认自己 正在使用哪个实例。

设计立场十分简单:品牌外观可替换;运行时信息不可替换。 自定义只允许用户把自己的品牌叠在上面,不允许屏蔽用于排障的关键 信息。本文档后续每一处「可改 / 不可改」的判定都来自这一立场。

对应 issue:#3005 

当前 Banner 由 Header(由 AppHeader 挂载)渲染,整体可拆分如下:

marginX=2 marginX=2 │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ ┌──── Logo 列 ─────────┐ gap=2 ┌──── 信息面板 (带边框) ──────────────┐ │ │ │ │ │ │ │ │ │ ███ QWEN ASCII ███ │ │ ① 标题: >_ Qwen Code (vX.Y.Z) │ │ │ │ ███ ART ART ███ │ │ ② 副标题: «空白行 / 自定义覆盖» │ │ │ │ ███ QWEN ASCII ███ │ │ ③ 状态: Qwen OAuth | qwen-… │ │ │ │ │ │ ④ 路径: ~/projects/example │ │ │ └──────── A ───────────┘ └──────────────── B ──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ 区域归属:AppHeader │ Tips 组件渲染在下方(由 ui.hideTips 控制) │

两个顶级区块:

  • A. Logo 列 —— 单块带渐变色的 ASCII art。 当前来源:packages/cli/src/ui/components/AsciiArt.ts 中的 shortAsciiLogo
  • B. 信息面板 —— 带边框的信息盒,共四行。第二行默认是空白视觉 spacer,可选地切换为调用方提供的副标题:
    • B① 标题:>_ Qwen Code (vX.Y.Z) —— 品牌文字 + 版本号后缀。
    • B② 副标题 / spacer:默认是单空格行,设置 ui.customBannerSubtitle 后渲染清洗后的单行副标题字符串(例如某个 fork 用 Built-in DataWorks Official Skills)。
    • B③ 状态:<鉴权显示类型> | <模型> ( /model 切换)
    • B④ 路径:经过 tildeify 与缩短的工作目录。

外层 <AppHeader> 已经基于 showBanner = !config.getScreenReader() 对 Banner 做了屏读模式下的整体隐藏处理(屏读模式下回退为纯文本输出)。

自定义规则 —— 哪些可改,哪些被锁定

区域当前来源自定义类别锁定/开放原因
A. Logo 列shortAsciiLogo (AsciiArt.ts)可替换 + 可自动隐藏纯品牌区域。白标场景需要完全控制视觉。窄终端下「自动隐藏 Logo」的现有行为保持不变。
B①. 标题文字>_ Qwen CodeHeader.tsx 硬编码可替换品牌区域。开头的 >_ 字符是现有品牌的一部分;如不需要,用户在 customBannerTitle 中省略即可。
B①. 版本号后缀(vX.Y.Z)version prop锁定排障与支持必备。隐藏后只能通过 --version 才能回答「你用的什么版本?」,对支持流程是真实成本。我们以小幅白标体验损失换取支持可达性。
B②. 副标题 / spacer 行默认空白可替换纯品牌 / 上下文区域。白标 fork 用它给构建版本打 tag(如 “Built-in DataWorks Official Skills”)。清洗规则与标题一致;只允许单行,不接受会破坏布局的换行。
B③. 状态行(鉴权 + 模型)formattedAuthTypemodel prop锁定运营与安全信号。用户必须看到当前使用的凭据以及实际消耗 token 的模型。任何隐藏/替换都是 footgun,即便在白标场景下也不应允许。
B④. 路径行(工作目录)workingDirectory prop锁定运营信息。「我现在在哪个目录?」是高频问题;Banner 是其唯一权威答案。
整个 Banner (A + B)AppHeader.tsx<Header> 挂载点可隐藏一个 ui.hideBanner: true 同时跳过 A、B 两个区块 —— 形态与现有屏读模式开关一致。<Tips> 仍由独立的 ui.hideTips 控制。

上述矩阵对应四个设置项,仅此而已:

设置默认值效果影响区域
ui.hideBannerfalse隐藏整个 Banner(区域 A + B)。A + B
ui.customBannerTitle未设置替换 B① 的品牌文字。版本号后缀照常追加。会被 trim;空字符串 = 使用默认。B① 品牌文字
ui.customBannerSubtitle未设置用一行副标题替换 B② 的空白 spacer。会被清洗;上限 160 字符;空字符串 = 保留空白 spacer(向后兼容)。B② spacer 行
ui.customAsciiArt未设置替换区域 A。支持三种数据形态(见下文)。任何错误均回退为默认。A
Намеренно не предоставляемые возможности:
  • Нет переключателя для «скрыть только суффикс версии».
  • Нет переключателя для «скрыть только строку аутентификации/модели».
  • Нет переключателя для «скрыть только строку пути».
  • Нет возможности изменить цвет градиента логотипа (цвета определяются темой).
  • Нет возможности изменить порядок или структуру информационной панели.

Если в будущем возникнет реальная потребность, её следует выносить как новое поле с отдельной оценкой, а не порождать из трёх перечисленных выше полей.

Руководство пользователя по настройке — как изменять

Обзор ограничений

Каждое изменение баннера подчиняется следующим группам лимитов. Перед тем как писать ASCII-арт вручную, просмотрите их, чтобы парсер не обрезал или не отклонил результат молча.

ПунктЛимит
Количество символов в заголовкеМаксимум 80 символов (счёт после очистки). При превышении обрезается с предупреждением [BANNER]. Символы перевода строки и управляющие удаляются до подсчёта.
Количество символов в подзаголовкеМаксимум 160 символов (счёт после очистки). Конвейер очистки такой же, как для заголовка; при превышении также выдаётся предупреждение [BANNER].
Размер блока ASCII artМаксимум 200 строк × 200 столбцов на уровень. При превышении обрезается с предупреждением [BANNER].
Размер файла ASCII artМаксимум 64 КБ. Если файл больше, читаются только первые 64 КБ, остальное игнорируется.
Реальная ширина рендеринга ASCII artОпределяется шириной терминала при запуске, не фиксированное число символов. Точная формула и доступные значения для разных ширин терминала — см. ниже «Насколько большим может быть логотип? — Бюджет ширины».

Для ASCII art нет фиксированного лимита по количеству символов — есть только две абсолютные границы по столбцам/строкам выше и бюджет ширины, вычисляемый по ширине терминала при запуске. Одно и то же название бренда из 17 символов может поместиться в одну строку или нет в зависимости от визуальной ширины шрифта, а не от количества букв.

Размещение конфигурации

Все четыре настройки находятся в узле ui файла settings.json. Поддерживаются настройки уровня пользователя (~/.qwen/settings.json) и уровня рабочей области (.qwen/settings.json в корне проекта). Приоритет слияния стандартный (рабочая область переопределяет пользователя, системный уровень переопределяет рабочую область).

customAsciiArt — исключение: парсер не заменяет весь объект целиком значением из более приоритетной области, а проходит по всем областям для каждого уровня (tier). Если настройка пользователя определяет { small }, а настройка рабочей области — { large }, то оба вступают в силу: small берётся от пользователя, large — от рабочей области. Это позволяет одновременно:

  1. Разрешать { path } относительно того файла, где он объявлен (.qwen/ рабочей области vs. ~/.qwen/ пользователя); при простом слиянии без учёта области эта информация теряется.
  2. Пользователь может оставить стандартный уровень large в личных настройках, а для конкретной рабочей области переопределить только small, не переписывая весь объект каждый раз.

Если один и тот же уровень определён в нескольких областях, действует обычный приоритет (системный > рабочая область > пользователь). Если в любой области customAsciiArt задан как простая строка или { path }, то он одновременно заполняет оба уровня для этой области.

Полное скрытие баннера

{ "ui": { "hideBanner": true, }, }

При запуске пропускаются колонка с логотипом и информационная панель. Если также не установлен ui.hideTips, подсказки всё равно отображаются.

Замена брендового заголовка

{ "ui": { "customBannerTitle": "Acme CLI", }, }

Информационная панель отобразит Acme CLI (vX.Y.Z). При установке пользовательского заголовка по умолчанию символы >_ не добавляются; если нужно их сохранить, укажите их явно: "customBannerTitle": ">_ Acme CLI".

Добавление брендового подзаголовка

{ "ui": { "customBannerSubtitle": "Built-in DataWorks Official Skills", }, }

Подзаголовок отображается отдельной строкой вторичным цветом текста, заменяя стандартную пустую строку-разделитель (которая находится между заголовком и строкой аутентификации/модели):

┌─────────────────────────────────────────────────────────┐ │ DataWorks DataAgent (vX.Y.Z) │ ← B① Заголовок │ Built-in DataWorks Official Skills │ ← B② Подзаголовок │ Qwen OAuth | qwen-coder ( /model для переключения) │ ← B③ Состояние │ ~/projects/example │ ← B④ Путь └─────────────────────────────────────────────────────────┘

Ограничения:

  • Только одна строка. Символы перевода строки и другие управляющие байты удаляются или заменяются пробелами, чтобы не нарушать макет панели.
  • После очистки — максимум 160 символов (немного больше, чем для заголовка; строки «при поддержке» и т.п. часто длиннее названия бренда).
  • Пустое значение (или пустая строка / строка из пробелов) = сохраняется стандартная пустая строка-разделитель — обратная совместимость по умолчанию.
  • Подзаголовок не влияет на поведение «закреплённых» строк; аутентификация, модель и рабочий каталог всегда видны независимо от наличия подзаголовка.

Замена ASCII art — встроенная строка

{ "ui": { "customAsciiArt": " ___ _ _ ____ \n / _ \\| | / |/ _\\\n| |_| | |__| | __/\n \\___/|____|_|___|", }, }

В JSON-строке перевод строки обозначается как \n. На этот ASCII art будет применён текущий градиент темы, как и для стандартного логотипа.

Нет ASCII art под рукой? Подойдёт любой внешний генератор; просто вставьте результат. Проще всего через figlet: npx figlet -f "ANSI Shadow" "xxxCode" > brand.txt, затем укажите customAsciiArt: { "path": "./brand.txt" }. CLI не выполняет рендеринг текста в ASCII art во время выполнения — причина см. ниже «Что не входит в область видимости».

Замена ASCII art — внешний файл

{ "ui": { "customAsciiArt": { "path": "./brand.txt" }, }, }

Избегайте экранирования больших многострочных строк в JSON. Правила разрешения путей:

  • Настройки уровня рабочей области: относительный путь разрешается относительно каталога .qwen/ рабочей области.
  • Настройки уровня пользователя: относительный путь разрешается относительно ~/.qwen/.
  • Абсолютные пути используются как есть.
  • Файл читается только один раз при запуске, очищается и кэшируется. Изменение файла во время сессии не приводит к перерисовке — перезапустите CLI.

Замена ASCII art — адаптивная ширина

{ "ui": { "customAsciiArt": { "small": " ACME\n ----", "large": { "path": "./brand-wide.txt" }, }, }, }

Если терминал достаточно широк, приоритетно используется large; иначе small; если ни один не подходит, колонка логотипа скрывается (по текущей двухколоночной стратегии). small и large могут быть как строкой, так и { path }. Любой уровень можно опустить: при отсутствии переходит к следующему.

Насколько большим может быть логотип? — Бюджет ширины

Ни у заголовка, ни у ASCII art нет «жёсткого лимита по количеству символов» — есть только бюджет ширины, зависящий от ширины терминала, и абсолютные жёсткие лимиты для защиты от деформирующего ввода:

ПунктЛимит
Ширина терминала при запускеСколько сообщит терминал пользователя.
Внешние отступы контейнера4 колонки (2 слева + 2 справа).
Промежуток между колонкой логотипа и информационной панелью2 колонки.
Минимальная ширина информационной панели44 колонки (40 путь + рамки + отступы).
Доступная ширина для рендеринга art каждого уровняширина_терминала − 4 − 2 − 44 = ширина_терминала − 50.
Жёсткий лимит art одного уровня после очистки200 колонок × 200 строк. При превышении обрезается с предупреждением [BANNER].
Жёсткий лимит customBannerTitle после очистки80 символов. При превышении обрезается с предупреждением [BANNER].

Соответствие между типичной шириной терминала и максимальной шириной логотипа:

Ширина терминалаМаксимальная ширина рендеринга логотипаЧто это означает на практике
8030Большинство букв figlet «ANSI Shadow» занимают 7–11 колонок, максимум 3 символа.
10050ANSI Shadow для короткого слова (примерно 6 букв) или два слова в стопку.
12070Для многострочного art в стопку достаточно.
200150Одна длинная строка (например, полное название продукта в ANSI Shadow) тоже поместится.
Два эмпирических правила при разработке art:
  1. Многословные названия брендов обычно не помещаются на одной строке в терминале при использовании шрифта ANSI Shadow. ANSI Shadow занимает около 7–9 столбцов на букву, даже для 12-символьного названия вроде Custom Agent требуется около 95 столбцов art — в терминале шириной 100 столбцов после размещения информационной панели места уже не хватает. Либо разбивайте слова по строкам, либо используйте более узкий figlet-шрифт, либо применяйте компактное однострочное оформление, например ▶ Custom Agent ◀.
  2. Когда один документ должен одновременно «хорошо выглядеть на широком экране» и «не умирать на узком», используйте адаптивную форму с шириной { small, large }. В примере ниже large — это многострочный art, уложенный для терминалов шириной ≥ 104 столбцов, small — однострочное оформление на 16 столбцов, а если ширина слишком мала для обоих, колонка с логотипом просто скрывается.
{ "ui": { "customBannerTitle": "Custom Agent", "customAsciiArt": { "small": "▶ Custom Agent ◀", "large": { "path": "./banner-large.txt" }, }, }, }

В banner-large.txt помещается уложенный вывод ANSI Shadow (примерно 54 столбца × 12 строк), его можно сгенерировать следующей командой:

( npx figlet -f "ANSI Shadow" CUSTOM npx figlet -f "ANSI Shadow" AGENT ) > banner-large.txt

Тройная комбинация

{ "ui": { "hideBanner": false, "customBannerTitle": "Acme CLI", "customAsciiArt": { "small": " ACME\n ----", "large": { "path": "./brand-wide.txt" }, }, }, }

Как проверить

  1. Сохраните settings.json, перезапустите qwen — разбор баннера выполняется только один раз при запуске.
  2. Измените ширину терминала, убедитесь, что переключение small / large работает как ожидается, а на очень узкой ширине колонка с логотипом корректно скрывается.
  3. Если результат не соответствует ожиданиям, посмотрите ~/.qwen/debug/<sessionId>.txt (символическая ссылка latest.txt указывает на текущую сессию), выполните поиск [BANNER] — каждая «мягкая» ошибка выводит строку warn с объяснением причины.

Конвейер разбора

settings.json packages/cli/src/ui/components/ ───────────── ────────────────────────────── { AppHeader.tsx "ui": { │ "hideBanner": false, │ showBanner = "customBannerTitle": "Acme", │ !screenReader "customBannerSubtitle": "Built-in …", │ && !ui.hideBanner "customAsciiArt": … │ } │ } ▼ │ <Header ▼ customAsciiArt={resolved.asciiArt} loadSettings() customBannerTitle={resolved.title} merge user / workspace customBannerSubtitle={resolved.subtitle} │ version=… model=… authType=… ▼ workingDirectory=… /> resolveCustomBanner(settings) │ ┌─────────────────────────┐ ▼ │ 1. Нормализация в │ packages/cli/src/ui/components/ │ { small, large } │ Header.tsx │ 2. Разбор каждого │ │ │ уровня: │ │ Выбор уровня по │ string → используем │ │ availableTerminalWidth │ {path} → fs.read │ ▼ │ O_NOFOLLOW │ Рендеринг колонки Logo │ ≤ 64 KB │ Рендеринг информационной панели: │ 3. Очистка art: │ Title = customBannerTitle │ stripControlSeqs │ ?? '>_ Qwen Code' │ ≤ 200 строк × 200 │ Subtitle = customBannerSubtitle │ столбцов │ ?? пустая строка-разделитель │ 4. Очистка title + │ Status = фиксировано │ subtitle (одна │ Path = фиксировано │ строка, ≤ 80 / 160 │ │ символов) │ │ 5. Memoization по │ │ источнику │ └─────────────────────────┘

Пятишаговый алгоритм разбора выполняется один раз при загрузке настроек и повторно только при событии горячей перезагрузки настроек:

  1. Нормализация. Простая string или { path } преобразуется в { small: x, large: x }. Объект { small, large } передаётся без изменений.
  2. Разбор по уровням. Для каждого AsciiArtSource:
    • строка: используется как есть.
    • { path }: синхронное чтение с использованием O_NOFOLLOW для защиты от атак на символические ссылки (на Windows откатывается к обычному read-only чтению — эта константа не экспортируется), лимит 64 КБ. Относительный путь считается относительно каталога файла настроек: workspace-настройки — относительно workspace .qwen/, пользовательские — относительно ~/.qwen/. Ошибка чтения → warn [BANNER], этот уровень откатывается к умолчанию.
  3. Очистка. Специализированный stripper для баннера: удаляет управляющие последовательности OSC/CSI/SS2/SS3, заменяет остальные управляющие байты C0/C1 (включая DEL) на пробелы, сохраняя \n для многострочного ASCII art. После обрезки хвостовых пробелов в каждой строке обрезается до 200 строк × 200 столбцов, лишнее отбрасывается с выводом warn [BANNER].
  4. Выбор уровня при рендеринге. В Header.tsx, имея очищенные small и large, в зависимости от доступной ширины (availableTerminalWidth ≥ logoWidth + logoGap + minInfoPanelWidth):
    • Если large помещается — приоритет large.
    • Иначе если small помещается — откат на small.
    • Иначе если пользователь предоставил кастомный art — колонка Logo просто скрывается (используется ветка showLogo = false) — в этом случае откат к встроенному логотипу QWEN на узком терминале незаметно нарушит white-label развёртывание. Информационная панель продолжает рендериться.
    • Иначе (пользователь вообще не предоставил кастомный art) — откат на shortAsciiLogo, отображаемый в зависимости от ширинного шлюза стандартного логотипа.
  5. Запасной вариант. Если оба уровня из-за «мягких» ошибок (отсутствие файла, пустота после очистки, некорректная конфигурация) в итоге пусты или невалидны, рендерится shortAsciiLogo как некастомизированный, с ширинным шлюзом стандартного логотипа. CLI ни в коем случае не должен падать из-за неправильной конфигурации баннера.

Псевдокод выбора уровня:

function pickTier( small: string | undefined, large: string | undefined, availableWidth: number, logoGap: number, minInfoPanelWidth: number, ): string | undefined { for (const candidate of [large, small]) { if (!candidate) continue; const w = getAsciiArtWidth(candidate); if (availableWidth >= w + logoGap + minInfoPanelWidth) { return candidate; } } return undefined; // скрыть колонку Logo }

Добавление в схему настроек

В packages/cli/src/config/settingsSchema.ts в объект ui сразу после shellOutputMaxLines добавляются четыре свойства:

hideBanner: { type: 'boolean', label: 'Hide Banner', category: 'UI', requiresRestart: false, default: false, description: 'Скрыть стартовый ASCII-баннер и информационную панель.', showInDialog: true, }, customBannerTitle: { type: 'string', label: 'Custom Banner Title', category: 'UI', requiresRestart: false, default: '' as string, description: 'Заменить стандартный заголовок ">_ Qwen Code", отображаемый в информационной панели баннера. Суффикс версии всегда добавляется.', showInDialog: false, }, customBannerSubtitle: { type: 'string', label: 'Custom Banner Subtitle', category: 'UI', requiresRestart: false, default: '' as string, description: 'Необязательная строка подзаголовка, отображаемая между заголовком баннера и строкой auth/model. Если не задана, информационная панель сохраняет пустую строку-разделитель.', showInDialog: false, }, customAsciiArt: { type: 'object', label: 'Custom ASCII Art', category: 'UI', requiresRestart: false, default: undefined, description: 'Заменить стандартный ASCII-арт QWEN. Принимает встроенную строку, {"path": "..."} или {"small": ..., "large": ...} для выбора в зависимости от ширины.', showInDialog: false, // Во время выполнения принимает объединённую форму, которую нельзя выразить через SettingDefinition `type`. // override выводится генератором JSON-схемы как есть, чтобы VS Code принимал все задокументированные формы (string, {path}, {small,large}) и не подсвечивал простые строки как ошибки. jsonSchemaOverride: { /* string | {path} | {small,large} oneOf … */ }, },

hideBanner использует существующий шаблон hideTips (showInDialog: true); Остальные три поля произвольного текста (заголовок, подзаголовок, art) не попадают в диалог настроек приложения — многострочный ASCII-редактор в диалоге TUI — это другой проект, продвинутые пользователи могут напрямую редактировать settings.json.

Изменения в коде

Изменения минимальны. Ниже указаны файлы и диапазоны строк в текущей ветке main.

packages/cli/src/ui/components/AppHeader.tsx:53 — расширение showBanner:

const showBanner = !config.getScreenReader() && !settings.merged.ui?.hideBanner;

packages/cli/src/ui/components/AppHeader.tsx — передача разобранных данных Banner в компонент <Header>:

<Header version={version} authDisplayType={authDisplayType} model={model} workingDirectory={targetDir} customAsciiArt={resolvedBanner?.asciiArt /* { small?, large? } */} customBannerTitle={resolvedBanner?.title /* string | undefined */} customBannerSubtitle={resolvedBanner?.subtitle /* string | undefined */} />

packages/cli/src/ui/components/Header.tsx — расширение HeaderProps:

interface HeaderProps { customAsciiArt?: { small?: string; large?: string }; customBannerTitle?: string; customBannerSubtitle?: string; version: string; authDisplayType?: AuthDisplayType; model: string; workingDirectory: string; }

packages/cli/src/ui/components/Header.tsx:45-46 — перед вычислением logoWidth выбирается уровень, с запасным вариантом по умолчанию:

const tier = pickTier( customAsciiArt?.small, customAsciiArt?.large, availableTerminalWidth, logoGap, minInfoPanelWidth, ); const displayLogo = tier ?? shortAsciiLogo;

packages/cli/src/ui/components/Header.tsx — заголовок отображается из пропа, подзаголовок заменяет пустую строку-разделитель, если проп истинен:

<Text bold color={theme.text.accent}> {customBannerTitle ? customBannerTitle : '>_ Qwen Code'} </Text> {customBannerSubtitle ? ( <Text color={theme.text.secondary}>{customBannerSubtitle}</Text> ) : ( <Text> </Text> )}

Новый файл: packages/cli/src/ui/utils/customBanner.ts — парсер. Внешний интерфейс:

export interface ResolvedBanner { asciiArt: { small?: string; large?: string }; title?: string; subtitle?: string; } export function resolveCustomBanner(settings: LoadedSettings): ResolvedBanner;

Парсер отвечает за нормализацию, чтение файлов, очистку и кэширование, описанные в «Конвейере разбора» выше. Вызывается один раз при запуске CLI и повторно при событии горячей перезагрузки настроек. Путь к файлу для каждого scope берётся непосредственно из settings.system.path / settings.workspace.path / settings.user.path, поэтому каждый { path } разрешается относительно файла, в котором он объявлен; когда settings.isTrusted имеет значение false, workspace scope полностью пропускается.

Сравнение альтернативных решений

Ниже приведены 5 оценённых форм для удобства последующих разработчиков, чтобы понимать пространство вариантов и при необходимости пересмотреть.

Вариант 1 — три плоских поля (рекомендуется, полностью соответствует issue)

{ "ui": { "customAsciiArt": "...", // string | {path} | {small,large} "customBannerTitle": "Acme CLI", "hideBanner": false, }, }
  • Результат: минимальный интерфейс для пользователя, полное соответствие описанию issue.
  • Преимущества: нулевой порог обучения; очень простая документация; согласованность с существующими плоскими полями ui.* (hideTips, customWittyPhrases и т.д.).
  • Недостатки: три семантически связанных ключа разбросаны на верхнем уровне ui; если в будущем появятся новые переключатели, специфичные для баннера (градиент, подзаголовок и т.д.), их придётся добавлять как братские поля в ui, без естественной группировки.

Вариант 2 — вложенное пространство имён ui.banner

{ "ui": { "banner": { "hide": false, "title": "Acme CLI", "asciiArt": { "path": "./brand.txt" }, }, }, }
  • Результат: функциональность идентична варианту 1, сгруппирована по функциям.
  • Преимущества: чистое пространство имён для будущих переключателей, специфичных для баннера; лучшая обнаруживаемость в /settings.
  • Недостатки: не полностью соответствует синтаксису исходного issue; существующие настройки UI в основном плоские (только ui.accessibility и ui.statusLine являются вложенными), нарушение согласованности; дополнительный уровень для запоминания.

Вариант 3 — пресеты профилей баннера + переопределение слотов

{ "ui": { "bannerProfile": "minimal" | "default" | "branded" | "hidden", "banner": { /* переопределение слотов для 'branded' */ } } }
  • Результат: пользователь выбирает из именованных пресетов; продвинутые пользователи переопределяют отдельные слоты в выбранном пресете.
  • Преимущества: лучший опыт onboarding; пресеты могут поставляться с CLI.
  • Недостатки: значительное усложнение; пресеты — это долгосрочное обязательство по поддержке; issue требует открытой кастомизации, а не контроля контента.

Вариант 4 — единый шаблон баннера в виде строки

{ "ui": { "bannerTemplate": "{{logo}}\n>_ {{title}} ({{version}})\n{{auth}} | {{model}}\n{{path}}", }, }
  • Результат: одна свободная строка шаблона с интерполяцией фиксированных полей.
  • Преимущества: максимальная гибкость для нестандартных макетов.
  • Недостатки: возлагает ответственность за макет на пользователя; теряется устойчивость двухколоночного макета Ink к ширине терминала; легко создать шаблон, который сломается на узких терминалах; слишком большой потенциал разрушения для такой выгоды.

Вариант 5 — API плагинов / хуков

Через систему расширений предоставить хук для рендеринга баннера.

  • Результат: кастомизация на уровне кода; расширение может отображать произвольное содержимое.
  • Преимущества: максимальная мощность; предприятие может упаковать готовый брендовый плагин.
  • Недостатки: огромная поверхность API; произвольный рендеринг терминала требует проверки безопасности; полный овердизайн для этого issue.

Рекомендация

Принять вариант 1. Он напрямую удовлетворяет issue, соответствует существующему стилю ui.* и не будет заблокирован пространством имён до того, как мы чётко определим, какие ещё переключатели, специфичные для баннера, появятся. Если в будущем начнут накапливаться братские поля, миграция на вариант 2 будет аддитивной — ui.banner.title и ui.customBannerTitle могут сосуществовать в период устаревания.

Безопасность и обработка ошибок

Пользовательский контент баннера выводится дословно в терминал, а в случае с формой path также читается с диска. Оба пути достижимы при загрузке вредоносных или поддельных настроек. Та же модель угроз, что и для функции session-title, применима здесь.

ПроблемаЗащита
Внедрение ANSI / OSC-8 / CSI в ASCII art / заголовок / подзаголовокСпециализированный stripper для баннера (sanitizeArt / sanitizeSingleLine): удаляет ESC-последовательности OSC / CSI / SS2 / SS3, заменяет остальные управляющие байты C0 / C1 (включая DEL) пробелами. Применяется как при рендеринге, так и перед записью в кэш.
Заморозка запуска из-за огромного файлаЖёсткое ограничение на чтение файла — 64 КБ.
Заморозка макета из-за патологического ASCII artМаксимум 200 строк × 200 столбцов на один результат парсинга; превышение обрезается + предупреждение [BANNER].
Атака с использованием символической ссылки в форме pathЧтение файла с флагом O_NOFOLLOW (на Windows — откат к только для чтения; константа не экспортируется).
Файл отсутствует или не читаетсяПерехват → предупреждение [BANNER] → откат к умолчанию; никогда не выбрасывается в UI.
Заголовок / подзаголовок содержит перевод строки или слишком длинныйСимволы перевода строки заменяются на пробелы, обрезается до 80 (заголовок) / 160 (подзаголовок) символов.
Недоверенная рабочая область влияет на рендеринг или чтение файловЕсли settings.isTrusted равно false, парсер полностью пропускает settings.workspace (согласовано с механизмом доверия для settings.merged).
Состояние гонки при горячей перезагрузке настроекРезультат парсинга мемоизируется по источнику (path или строка) в рамках каждого вызова; при reload парсер запускается заново и перечитывает затронутые файлы.
失败模式总结:所有软失败最终都会落到 shortAsciiLogo(或锁定的默认标题)+ 一行调试日志 warn。任何分支都不允许产生硬失败(向上抛出异常)。

不在本设计范围内

下列项被有意排除。每一项都可以视用户反馈做后续单独提案。

项目不做的理由
文案转 ASCII art({ text: "xxxCode" } 形态)v1 评估后拒绝。要么引入 figlet 运行时依赖(含一套可用字体后约 2–3 MB unpacked),要么自己 vendor 一份单字体渲染器(~200 行代码 + 一份 .flf 字体我们自己维护)。两条路都带来长期的维护面:字体选型、字体 license 审计、「我的字体在 X 终端渲染不对」类 issue、CJK / 全角字符处理。本特性的驱动用例(白标 / 多租户)几乎一定有设计师交付成品 ASCII art,不会依赖 figlet 默认字体。希望一行命令生成的用户今天就能 npx figlet "xxxCode" > brand.txt + customAsciiArt: { "path": "./brand.txt" } —— 等价效果、零新增依赖、零 Qwen Code 内部支持负担。如果未来诉求增多,这一形态是纯叠加:把 AsciiArtSource 扩展为 string | {path} | {text, font?},不会破坏任何已有配置。
/banner slash 命令在线编辑设置 UI 是规范化的编辑入口;多行 ASCII 在线编辑器是另一个项目。
自定义渐变色 / 单行颜色颜色由 theme 拥有。如需扩展应另立提案,Banner 自定义不重复造该面。
URL 加载 ASCII art启动期网络请求自带一堆问题:失败模式、缓存、安全评审。{path} 文件加载是低风险等价物。
动画(旋转 Logo、跑马灯标题)增加渲染负担与无障碍问题;本特性的用例不需要。
VSCode / Web UI banner 对齐这两个端目前不渲染 Ink Banner。若未来引入,本设计为参考。
文件变更的动态 reload解析器仅在启动与设置 reload 时运行。会话中途换 art 的需求很少,「重启生效」是可以接受的折中。
单独隐藏锁定区域(version / auth / model / path)这些是运行时信号;屏蔽它们对支持与安全姿态的损害,远大于白标场景的收益。

План проверки

Последующие реализации PR должны проходить следующие сквозные проверки:

  1. Установка в ~/.qwen/settings.json параметра customBannerTitle: "Acme CLI" вместе с встроенным customAsciiArt → при запуске qwen отображается новый заголовок и новый ASCII‑арт; суффикс версии остаётся.
  2. Установка customBannerSubtitle: "Built-in Acme Skills" → строка подзаголовка отображается второстепенным цветом между заголовком и строкой аутентификации / модели; аутентификация, модель, путь остаются видимыми. После снятия настройки возвращается пустая строка‑разделитель (обратная совместимость).
  3. Установка hideBanner: true → при запуске qwen баннер не показывается; подсказки и основной контент отображаются как обычно.
  4. В workspace settings.json задан customAsciiArt: { "path": "./brand.txt" }, файл brand.txt находится в том же каталоге .qwen/ → при открытии рабочей области загружается с диска.
  5. customAsciiArt: { "small": "...", "large": "..." } → при изменении размера терминала (широкий / средний / узкий) для широкого используется large, для среднего – small, при узком колонка логотипа скрывается; информационная панель всегда видна.
  6. В customBannerTitle и customBannerSubtitle вставлен \x1b[31mhostile → оба места отображают текст буквально, без интерпретации как красного цвета.
  7. В path указан несуществующий файл → CLI нормально запускается; в ~/.qwen/debug/<sessionId>.txt появляется предупреждение [BANNER] warn; отображается стандартный art.
  8. При открытии рабочего дерева с отключённым доверием к рабочей области → предоставленный рабочей областью customAsciiArt (с элементом { path }) молча игнорируется; настройки пользователя по‑прежнему действуют.
Last updated on