feat(composer): fit footer controls by overflow (#4657, rebased onto master)
Rebased Paladin173's #4657 onto current master. The per-control visibility toggles half already shipped via his own #4598, so this rebase carries only the net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver + MutationObserver measure .composer-left overflow, step full -> cf-icons -> cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
This commit is contained in:
1 parent
1c4096aa9d
commit
c6370235be
12 files changed
+454
-222
No files matched your search
+8
-1
@@ -7734,7 +7734,8 @@ _SETTINGS_DEFAULTS = {
|
|||||||
"onboarding_completed": False,
|
"onboarding_completed": False,
|
||||||
"send_key": "enter", # 'enter' or 'ctrl+enter'
|
"send_key": "enter", # 'enter' or 'ctrl+enter'
|
||||||
"show_token_usage": False, # show input/output token badge below assistant messages
|
"show_token_usage": False, # show input/output token badge below assistant messages
|
||||||
"show_quota_chip": False, # show ambient provider quota chip in composer footer (default off; wide desktop only when enabled, see style.css @media)
|
"show_quota_chip": True, # show ambient provider quota chip in composer footer by default
|
||||||
|
"show_quota_chip_opt_out": False, # post-default-flip marker for explicit user opt-out
|
||||||
"show_conversation_outline": False, # show opt-in desktop jump-to-question outline panel
|
"show_conversation_outline": False, # show opt-in desktop jump-to-question outline panel
|
||||||
"hide_empty_state_suggestions": False, # hide the default new-chat suggestion buttons
|
"hide_empty_state_suggestions": False, # hide the default new-chat suggestion buttons
|
||||||
"virtualize_transcript": False, # #4343: virtualize long (>80 msg) transcripts. EXPERIMENTAL, opt-IN (default OFF). Was opt-out/default-on in #4325 but caused scroll-up flicker on long sessions with tall tool-call rows (variable-height anchor oscillation) — flipped off for everyone in #4343; re-enabling requires an explicit opt-in (see virtualize_transcript_optin migration in load_settings).
|
"virtualize_transcript": False, # #4343: virtualize long (>80 msg) transcripts. EXPERIMENTAL, opt-IN (default OFF). Was opt-out/default-on in #4325 but caused scroll-up flicker on long sessions with tall tool-call rows (variable-height anchor oscillation) — flipped off for everyone in #4343; re-enabling requires an explicit opt-in (see virtualize_transcript_optin migration in load_settings).
|
||||||
@@ -7934,6 +7935,11 @@ def load_settings() -> dict:
|
|||||||
# Honor a stored True only when that marker is present.
|
# Honor a stored True only when that marker is present.
|
||||||
if not bool(stored.get("virtualize_transcript_optin")):
|
if not bool(stored.get("virtualize_transcript_optin")):
|
||||||
settings["virtualize_transcript"] = False
|
settings["virtualize_transcript"] = False
|
||||||
|
# show_quota_chip used to be persisted as False by default. After
|
||||||
|
# flipping it visible-by-default, treat an old stored False as
|
||||||
|
# stale unless a post-flip opt-out marker is present.
|
||||||
|
if not bool(stored.get("show_quota_chip_opt_out")):
|
||||||
|
settings["show_quota_chip"] = True
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Failed to load settings from %s", SETTINGS_FILE)
|
logger.debug("Failed to load settings from %s", SETTINGS_FILE)
|
||||||
@@ -7978,6 +7984,7 @@ _SETTINGS_BOOL_KEYS = {
|
|||||||
"onboarding_completed",
|
"onboarding_completed",
|
||||||
"show_token_usage",
|
"show_token_usage",
|
||||||
"show_quota_chip",
|
"show_quota_chip",
|
||||||
|
"show_quota_chip_opt_out",
|
||||||
"show_conversation_outline",
|
"show_conversation_outline",
|
||||||
"hide_empty_state_suggestions",
|
"hide_empty_state_suggestions",
|
||||||
"virtualize_transcript",
|
"virtualize_transcript",
|
||||||
|
|||||||
+3
-3
@@ -2295,7 +2295,7 @@ const _COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS=[
|
|||||||
{key:'hide_composer_yolo',label:'YOLO',labelKey:'composer_control_yolo',selectors:['#yoloPill']},
|
{key:'hide_composer_yolo',label:'YOLO',labelKey:'composer_control_yolo',selectors:['#yoloPill']},
|
||||||
{key:'hide_composer_bg_badge',label:'Background badge',labelKey:'composer_control_bg_badge',selectors:['#bgBadge']},
|
{key:'hide_composer_bg_badge',label:'Background badge',labelKey:'composer_control_bg_badge',selectors:['#bgBadge']},
|
||||||
{key:'hide_composer_mobile_config',label:'Mobile config',labelKey:'composer_control_mobile_config',selectors:['#composerMobileConfigBtn']},
|
{key:'hide_composer_mobile_config',label:'Mobile config',labelKey:'composer_control_mobile_config',selectors:['#composerMobileConfigBtn']},
|
||||||
{key:'hide_composer_quota_chip',label:'Quota chip',labelKey:'composer_control_quota_chip',selectors:['#providerQuotaChip']},
|
{key:'hide_composer_quota_chip',label:'Quota chip',labelKey:'composer_control_quota_chip',selectors:['#providerQuotaChip','#composerMobileQuotaAction']},
|
||||||
{key:'hide_composer_toolsets',label:'Toolsets',labelKey:'composer_control_toolsets',selectors:['#composerToolsetsWrap']},
|
{key:'hide_composer_toolsets',label:'Toolsets',labelKey:'composer_control_toolsets',selectors:['#composerToolsetsWrap']},
|
||||||
{key:'hide_composer_status',label:'Status',labelKey:'composer_control_status',selectors:['#composerStatus']},
|
{key:'hide_composer_status',label:'Status',labelKey:'composer_control_status',selectors:['#composerStatus']},
|
||||||
];
|
];
|
||||||
@@ -2370,7 +2370,7 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
|||||||
// and workspace actions (New file/folder) work before the first session (#804).
|
// and workspace actions (New file/folder) work before the first session (#804).
|
||||||
if(s.default_workspace) S._profileDefaultWorkspace=s.default_workspace;
|
if(s.default_workspace) S._profileDefaultWorkspace=s.default_workspace;
|
||||||
window._showTokenUsage=!!s.show_token_usage;
|
window._showTokenUsage=!!s.show_token_usage;
|
||||||
window._showQuotaChip=s.show_quota_chip===true;
|
window._showQuotaChip=s.show_quota_chip!==false;
|
||||||
window._showConversationOutline=s.show_conversation_outline===true;
|
window._showConversationOutline=s.show_conversation_outline===true;
|
||||||
document.documentElement.dataset.conversationOutline=window._showConversationOutline?'enabled':'disabled';
|
document.documentElement.dataset.conversationOutline=window._showConversationOutline?'enabled':'disabled';
|
||||||
if(typeof applyConversationOutlinePreference==='function') applyConversationOutlinePreference();
|
if(typeof applyConversationOutlinePreference==='function') applyConversationOutlinePreference();
|
||||||
@@ -2504,7 +2504,7 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
|||||||
}catch(e){
|
}catch(e){
|
||||||
window._sendKey='enter';
|
window._sendKey='enter';
|
||||||
window._showTokenUsage=false;
|
window._showTokenUsage=false;
|
||||||
window._showQuotaChip=false;
|
window._showQuotaChip=true;
|
||||||
window._showConversationOutline=false;
|
window._showConversationOutline=false;
|
||||||
document.documentElement.dataset.conversationOutline='disabled';
|
document.documentElement.dataset.conversationOutline='disabled';
|
||||||
if(typeof applyConversationOutlinePreference==='function') applyConversationOutlinePreference();
|
if(typeof applyConversationOutlinePreference==='function') applyConversationOutlinePreference();
|
||||||
|
|||||||
+28
-14
@@ -811,7 +811,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Retry',
|
settings_autosave_retry: 'Retry',
|
||||||
settings_label_language: 'Language',
|
settings_label_language: 'Language',
|
||||||
settings_label_quota_chip: 'Show provider quota chip in composer',
|
settings_label_quota_chip: 'Show provider quota chip in composer',
|
||||||
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Default off. Only visible on wide displays (≥1400px) when enabled, to keep the composer uncluttered on laptop and standard desktop widths.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Show conversation outline',
|
settings_label_conversation_outline: 'Show conversation outline',
|
||||||
settings_desc_conversation_outline: 'Show a desktop-only jump list for user questions in the current conversation. Off by default.',
|
settings_desc_conversation_outline: 'Show a desktop-only jump list for user questions in the current conversation. Off by default.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -1600,6 +1600,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Type a message to send',
|
composer_disabled_empty: 'Type a message to send',
|
||||||
composer_mobile_workspace: 'Workspace',
|
composer_mobile_workspace: 'Workspace',
|
||||||
composer_mobile_model: 'Model',
|
composer_mobile_model: 'Model',
|
||||||
|
composer_mobile_quota: 'Quota',
|
||||||
composer_mobile_reasoning: 'Reasoning',
|
composer_mobile_reasoning: 'Reasoning',
|
||||||
composer_mobile_context: 'Context',
|
composer_mobile_context: 'Context',
|
||||||
media_audio_label: 'Audio',
|
media_audio_label: 'Audio',
|
||||||
@@ -2449,7 +2450,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Riprova',
|
settings_autosave_retry: 'Riprova',
|
||||||
settings_label_language: 'Lingua',
|
settings_label_language: 'Lingua',
|
||||||
settings_label_quota_chip: 'Mostra il chip della quota del provider nel compositore',
|
settings_label_quota_chip: 'Mostra il chip della quota del provider nel compositore',
|
||||||
settings_desc_quota_chip: "Mostra un indicatore di quota residua (es. saldo crediti OpenRouter) nel piè di pagina del compositore. Predefinito disattivato. Visibile solo su schermi larghi (≥1400px) quando attivato, per mantenere il compositore non affollato su laptop e desktop standard.",
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Mostra struttura conversazione',
|
settings_label_conversation_outline: 'Mostra struttura conversazione',
|
||||||
settings_desc_conversation_outline: 'Mostra un elenco di salto solo desktop per le domande utente nella conversazione corrente. Disattivato per impostazione predefinita.',
|
settings_desc_conversation_outline: 'Mostra un elenco di salto solo desktop per le domande utente nella conversazione corrente. Disattivato per impostazione predefinita.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -3226,6 +3227,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Scrivi un messaggio da inviare',
|
composer_disabled_empty: 'Scrivi un messaggio da inviare',
|
||||||
composer_mobile_workspace: 'Workspace',
|
composer_mobile_workspace: 'Workspace',
|
||||||
composer_mobile_model: 'Modello',
|
composer_mobile_model: 'Modello',
|
||||||
|
composer_mobile_quota: 'Quota',
|
||||||
composer_mobile_reasoning: 'Ragionamento',
|
composer_mobile_reasoning: 'Ragionamento',
|
||||||
composer_mobile_context: 'Contesto',
|
composer_mobile_context: 'Contesto',
|
||||||
media_audio_label: 'Audio',
|
media_audio_label: 'Audio',
|
||||||
@@ -4078,7 +4080,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: '再試行',
|
settings_autosave_retry: '再試行',
|
||||||
settings_label_language: '言語',
|
settings_label_language: '言語',
|
||||||
settings_label_quota_chip: 'コンポーザーにプロバイダーのクォータチップを表示',
|
settings_label_quota_chip: 'コンポーザーにプロバイダーのクォータチップを表示',
|
||||||
settings_desc_quota_chip: 'コンポーザーのフッターに残りクォータインジケーター(例: OpenRouter のクレジット残高)を表示します。デフォルトはオフ。有効にした場合、ラップトップや標準デスクトップの幅でコンポーザーが混雑しないよう、ワイドディスプレイ(≥1400px)でのみ表示されます。',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: '会話アウトラインを表示',
|
settings_label_conversation_outline: '会話アウトラインを表示',
|
||||||
settings_desc_conversation_outline: '現在の会話内のユーザー質問へ移動するデスクトップ専用リストを表示します。デフォルトはオフです。',
|
settings_desc_conversation_outline: '現在の会話内のユーザー質問へ移動するデスクトップ専用リストを表示します。デフォルトはオフです。',
|
||||||
settings_label_hide_suggestions: '新規チャットの候補を非表示',
|
settings_label_hide_suggestions: '新規チャットの候補を非表示',
|
||||||
@@ -4860,6 +4862,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: '送信するメッセージを入力してください',
|
composer_disabled_empty: '送信するメッセージを入力してください',
|
||||||
composer_mobile_workspace: 'ワークスペース',
|
composer_mobile_workspace: 'ワークスペース',
|
||||||
composer_mobile_model: 'モデル',
|
composer_mobile_model: 'モデル',
|
||||||
|
composer_mobile_quota: 'クォータ',
|
||||||
composer_mobile_reasoning: '思考',
|
composer_mobile_reasoning: '思考',
|
||||||
composer_mobile_context: 'コンテキスト',
|
composer_mobile_context: 'コンテキスト',
|
||||||
media_audio_label: '音声',
|
media_audio_label: '音声',
|
||||||
@@ -5421,7 +5424,7 @@ const LOCALES = {
|
|||||||
settings_label_theme: 'Тема',
|
settings_label_theme: 'Тема',
|
||||||
settings_label_language: 'Язык',
|
settings_label_language: 'Язык',
|
||||||
settings_label_quota_chip: 'Показывать чип квоты провайдера в композиторе',
|
settings_label_quota_chip: 'Показывать чип квоты провайдера в композиторе',
|
||||||
settings_desc_quota_chip: 'Отображает фоновый индикатор остатка квоты (например, баланс кредитов OpenRouter) в подвале композитора. По умолчанию отключено. Виден только на широких экранах (≥1400px) при включении, чтобы не загромождать композитор на ноутбуках и стандартных мониторах.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Показывать структуру беседы',
|
settings_label_conversation_outline: 'Показывать структуру беседы',
|
||||||
settings_desc_conversation_outline: 'Показывает на рабочем столе список переходов к вопросам пользователя в текущей беседе. По умолчанию отключено.',
|
settings_desc_conversation_outline: 'Показывает на рабочем столе список переходов к вопросам пользователя в текущей беседе. По умолчанию отключено.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -6357,6 +6360,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Введите сообщение для отправки',
|
composer_disabled_empty: 'Введите сообщение для отправки',
|
||||||
composer_mobile_workspace: 'Рабочее пространство',
|
composer_mobile_workspace: 'Рабочее пространство',
|
||||||
composer_mobile_model: 'Модель',
|
composer_mobile_model: 'Модель',
|
||||||
|
composer_mobile_quota: 'Квота',
|
||||||
composer_mobile_reasoning: 'Рассуждение',
|
composer_mobile_reasoning: 'Рассуждение',
|
||||||
composer_mobile_context: 'Контекст',
|
composer_mobile_context: 'Контекст',
|
||||||
media_audio_label: 'Аудио',
|
media_audio_label: 'Аудио',
|
||||||
@@ -6977,7 +6981,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Reintentar',
|
settings_autosave_retry: 'Reintentar',
|
||||||
settings_label_language: 'Idioma',
|
settings_label_language: 'Idioma',
|
||||||
settings_label_quota_chip: 'Mostrar el chip de cuota del proveedor en el compositor',
|
settings_label_quota_chip: 'Mostrar el chip de cuota del proveedor en el compositor',
|
||||||
settings_desc_quota_chip: 'Muestra un indicador ambiental de cuota restante (por ejemplo, saldo de crédito de OpenRouter) en el pie del compositor. Predeterminado: desactivado. Solo visible en pantallas anchas (≥1400px) cuando se activa, para mantener el compositor despejado en portátiles y monitores estándar.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Mostrar esquema de conversación',
|
settings_label_conversation_outline: 'Mostrar esquema de conversación',
|
||||||
settings_desc_conversation_outline: 'Muestra una lista de salto solo en escritorio para las preguntas del usuario en la conversación actual. Desactivado de forma predeterminada.',
|
settings_desc_conversation_outline: 'Muestra una lista de salto solo en escritorio para las preguntas del usuario en la conversación actual. Desactivado de forma predeterminada.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -7911,6 +7915,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Escribe un mensaje para enviar',
|
composer_disabled_empty: 'Escribe un mensaje para enviar',
|
||||||
composer_mobile_workspace: 'Espacio de trabajo',
|
composer_mobile_workspace: 'Espacio de trabajo',
|
||||||
composer_mobile_model: 'Modelo',
|
composer_mobile_model: 'Modelo',
|
||||||
|
composer_mobile_quota: 'Cuota',
|
||||||
composer_mobile_reasoning: 'Razonamiento',
|
composer_mobile_reasoning: 'Razonamiento',
|
||||||
composer_mobile_context: 'Contexto',
|
composer_mobile_context: 'Contexto',
|
||||||
media_audio_label: 'Audio',
|
media_audio_label: 'Audio',
|
||||||
@@ -8521,7 +8526,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Wiederholen',
|
settings_autosave_retry: 'Wiederholen',
|
||||||
settings_label_language: 'Sprache',
|
settings_label_language: 'Sprache',
|
||||||
settings_label_quota_chip: 'Anbieter-Kontingent-Chip im Editor anzeigen',
|
settings_label_quota_chip: 'Anbieter-Kontingent-Chip im Editor anzeigen',
|
||||||
settings_desc_quota_chip: 'Zeigt einen Hintergrund-Indikator des verbleibenden Kontingents (z. B. OpenRouter-Guthaben) in der Editor-Fußzeile an. Standardmäßig deaktiviert. Bei Aktivierung nur auf breiten Bildschirmen (≥1400px) sichtbar, damit der Editor auf Laptops und Standard-Desktops übersichtlich bleibt.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Konversationsgliederung anzeigen',
|
settings_label_conversation_outline: 'Konversationsgliederung anzeigen',
|
||||||
settings_desc_conversation_outline: 'Zeigt eine Desktop-Sprungliste für Benutzerfragen in der aktuellen Konversation. Standardmäßig deaktiviert.',
|
settings_desc_conversation_outline: 'Zeigt eine Desktop-Sprungliste für Benutzerfragen in der aktuellen Konversation. Standardmäßig deaktiviert.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -9149,6 +9154,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Nachricht eingeben zum Senden',
|
composer_disabled_empty: 'Nachricht eingeben zum Senden',
|
||||||
composer_mobile_workspace: 'Arbeitsbereich',
|
composer_mobile_workspace: 'Arbeitsbereich',
|
||||||
composer_mobile_model: 'Modell',
|
composer_mobile_model: 'Modell',
|
||||||
|
composer_mobile_quota: 'Kontingent',
|
||||||
composer_mobile_reasoning: 'Reasoning',
|
composer_mobile_reasoning: 'Reasoning',
|
||||||
composer_mobile_context: 'Kontext',
|
composer_mobile_context: 'Kontext',
|
||||||
model_custom_label: 'Benutzerdefinierte Modell-ID',
|
model_custom_label: 'Benutzerdefinierte Modell-ID',
|
||||||
@@ -10130,7 +10136,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: '重试',
|
settings_autosave_retry: '重试',
|
||||||
settings_label_language: '语言',
|
settings_label_language: '语言',
|
||||||
settings_label_quota_chip: '在编辑器中显示供应商配额标签',
|
settings_label_quota_chip: '在编辑器中显示供应商配额标签',
|
||||||
settings_desc_quota_chip: '在编辑器底部显示剩余配额指示器(如 OpenRouter 信用余额)。默认关闭。启用时仅在宽屏(≥1400px)显示,以保持笔记本和标准桌面屏幕上编辑器的整洁。',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: '显示会话大纲',
|
settings_label_conversation_outline: '显示会话大纲',
|
||||||
settings_desc_conversation_outline: '显示仅限桌面的跳转列表,用于跳转到当前会话中的用户问题。默认关闭。',
|
settings_desc_conversation_outline: '显示仅限桌面的跳转列表,用于跳转到当前会话中的用户问题。默认关闭。',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -11041,6 +11047,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: '请输入消息后发送',
|
composer_disabled_empty: '请输入消息后发送',
|
||||||
composer_mobile_workspace: '工作区',
|
composer_mobile_workspace: '工作区',
|
||||||
composer_mobile_model: '模型',
|
composer_mobile_model: '模型',
|
||||||
|
composer_mobile_quota: '配额',
|
||||||
composer_mobile_reasoning: '推理',
|
composer_mobile_reasoning: '推理',
|
||||||
composer_mobile_context: '上下文',
|
composer_mobile_context: '上下文',
|
||||||
|
|
||||||
@@ -11974,7 +11981,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: '重試',
|
settings_autosave_retry: '重試',
|
||||||
settings_label_language: '語言',
|
settings_label_language: '語言',
|
||||||
settings_label_quota_chip: '在編輯器中顯示供應商配額標籤',
|
settings_label_quota_chip: '在編輯器中顯示供應商配額標籤',
|
||||||
settings_desc_quota_chip: '在編輯器底部顯示剩餘配額指示器(如 OpenRouter 點數餘額)。預設關閉。啟用時僅在寬螢幕(≥1400px)顯示,以保持筆記型電腦和標準桌面螢幕上編輯器的整潔。',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: '顯示對話大綱',
|
settings_label_conversation_outline: '顯示對話大綱',
|
||||||
settings_desc_conversation_outline: '顯示僅限桌面的跳轉清單,用於跳轉到目前對話中的使用者問題。預設關閉。',
|
settings_desc_conversation_outline: '顯示僅限桌面的跳轉清單,用於跳轉到目前對話中的使用者問題。預設關閉。',
|
||||||
settings_label_hide_suggestions: '隱藏新聊天建議',
|
settings_label_hide_suggestions: '隱藏新聊天建議',
|
||||||
@@ -12759,6 +12766,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: '請輸入訊息後傳送',
|
composer_disabled_empty: '請輸入訊息後傳送',
|
||||||
composer_mobile_workspace: '工作區',
|
composer_mobile_workspace: '工作區',
|
||||||
composer_mobile_model: '模型',
|
composer_mobile_model: '模型',
|
||||||
|
composer_mobile_quota: '配額',
|
||||||
composer_mobile_reasoning: '推理',
|
composer_mobile_reasoning: '推理',
|
||||||
composer_mobile_context: '脈絡',
|
composer_mobile_context: '脈絡',
|
||||||
media_audio_label: '音訊',
|
media_audio_label: '音訊',
|
||||||
@@ -12922,6 +12930,7 @@ const LOCALES = {
|
|||||||
model_not_found_label: 'Modelo não encontrado',
|
model_not_found_label: 'Modelo não encontrado',
|
||||||
composer_mobile_workspace: 'Workspace',
|
composer_mobile_workspace: 'Workspace',
|
||||||
composer_mobile_model: 'Modelo',
|
composer_mobile_model: 'Modelo',
|
||||||
|
composer_mobile_quota: 'Cota',
|
||||||
composer_mobile_reasoning: 'Raciocínio',
|
composer_mobile_reasoning: 'Raciocínio',
|
||||||
composer_mobile_context: 'Contexto',
|
composer_mobile_context: 'Contexto',
|
||||||
model_custom_label: 'ID de modelo customizado',
|
model_custom_label: 'ID de modelo customizado',
|
||||||
@@ -13497,7 +13506,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Tentar novamente',
|
settings_autosave_retry: 'Tentar novamente',
|
||||||
settings_label_language: 'Idioma',
|
settings_label_language: 'Idioma',
|
||||||
settings_label_quota_chip: 'Mostrar o chip de cota do provedor no compositor',
|
settings_label_quota_chip: 'Mostrar o chip de cota do provedor no compositor',
|
||||||
settings_desc_quota_chip: 'Exibe um indicador ambiente de cota restante (por exemplo, saldo de crédito do OpenRouter) no rodapé do compositor. Desativado por padrão. Visível apenas em telas largas (≥1400px) quando ativado, para manter o compositor livre em laptops e monitores padrão.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Mostrar estrutura da conversa',
|
settings_label_conversation_outline: 'Mostrar estrutura da conversa',
|
||||||
settings_desc_conversation_outline: 'Mostra uma lista de atalhos somente no desktop para as perguntas do usuário na conversa atual. Desativado por padrão.',
|
settings_desc_conversation_outline: 'Mostra uma lista de atalhos somente no desktop para as perguntas do usuário na conversa atual. Desativado por padrão.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -15026,7 +15035,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: '다시 시도',
|
settings_autosave_retry: '다시 시도',
|
||||||
settings_label_language: '언어',
|
settings_label_language: '언어',
|
||||||
settings_label_quota_chip: '작성기에 공급자 할당량 칩 표시',
|
settings_label_quota_chip: '작성기에 공급자 할당량 칩 표시',
|
||||||
settings_desc_quota_chip: '작성기 푸터에 남은 할당량 표시기(예: OpenRouter 크레딧 잔액)를 표시합니다. 기본값은 끔. 활성화 시 노트북과 표준 데스크톱에서 작성기가 복잡해지지 않도록 와이드 디스플레이(≥1400px)에서만 표시됩니다.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: '대화 개요 표시',
|
settings_label_conversation_outline: '대화 개요 표시',
|
||||||
settings_desc_conversation_outline: '현재 대화의 사용자 질문으로 이동하는 데스크톱 전용 목록을 표시합니다. 기본값은 꺼짐입니다.',
|
settings_desc_conversation_outline: '현재 대화의 사용자 질문으로 이동하는 데스크톱 전용 목록을 표시합니다. 기본값은 꺼짐입니다.',
|
||||||
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
settings_label_hide_suggestions: 'Hide new-chat suggestions',
|
||||||
@@ -15742,6 +15751,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: '메시지를 입력하세요',
|
composer_disabled_empty: '메시지를 입력하세요',
|
||||||
composer_mobile_workspace: '워크스페이스',
|
composer_mobile_workspace: '워크스페이스',
|
||||||
composer_mobile_model: '모델',
|
composer_mobile_model: '모델',
|
||||||
|
composer_mobile_quota: '할당량',
|
||||||
composer_mobile_reasoning: '추론',
|
composer_mobile_reasoning: '추론',
|
||||||
composer_mobile_context: '컨텍스트',
|
composer_mobile_context: '컨텍스트',
|
||||||
|
|
||||||
@@ -16667,7 +16677,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Réessayer',
|
settings_autosave_retry: 'Réessayer',
|
||||||
settings_label_language: 'Langue',
|
settings_label_language: 'Langue',
|
||||||
settings_label_quota_chip: 'Afficher le jeton de quota du fournisseur dans le compositeur',
|
settings_label_quota_chip: 'Afficher le jeton de quota du fournisseur dans le compositeur',
|
||||||
settings_desc_quota_chip: 'Affiche un indicateur de quota restant (ex. solde de crédit OpenRouter) dans le pied de page du compositeur. Désactivé par défaut. Visible uniquement sur les grands écrans (≥1400px) lorsqu\'il est activé, pour garder le compositeur épuré sur les ordinateurs portables et les écrans de bureau standards.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Afficher le plan de la conversation',
|
settings_label_conversation_outline: 'Afficher le plan de la conversation',
|
||||||
settings_desc_conversation_outline: 'Afficher une liste de saut réservée au bureau pour les questions utilisateur dans la conversation actuelle. Désactivé par défaut.',
|
settings_desc_conversation_outline: 'Afficher une liste de saut réservée au bureau pour les questions utilisateur dans la conversation actuelle. Désactivé par défaut.',
|
||||||
settings_label_hide_suggestions: 'Masquer les suggestions de nouvelle conversation',
|
settings_label_hide_suggestions: 'Masquer les suggestions de nouvelle conversation',
|
||||||
@@ -17435,6 +17445,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Saisissez un message à envoyer',
|
composer_disabled_empty: 'Saisissez un message à envoyer',
|
||||||
composer_mobile_workspace: 'Espace de travail',
|
composer_mobile_workspace: 'Espace de travail',
|
||||||
composer_mobile_model: 'Modèle',
|
composer_mobile_model: 'Modèle',
|
||||||
|
composer_mobile_quota: 'Quota',
|
||||||
composer_mobile_reasoning: 'Raisonnement',
|
composer_mobile_reasoning: 'Raisonnement',
|
||||||
composer_mobile_context: 'Contexte',
|
composer_mobile_context: 'Contexte',
|
||||||
media_audio_label: 'Audio',
|
media_audio_label: 'Audio',
|
||||||
@@ -18264,7 +18275,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Yeniden dene',
|
settings_autosave_retry: 'Yeniden dene',
|
||||||
settings_label_language: 'Dil',
|
settings_label_language: 'Dil',
|
||||||
settings_label_quota_chip: 'Bestecide sağlayıcı kota çipini göster',
|
settings_label_quota_chip: 'Bestecide sağlayıcı kota çipini göster',
|
||||||
settings_desc_quota_chip: 'Besteci altbilgisinde bir ortam kalan kota göstergesini (örn. OpenRouter kredi bakiyesi) görüntüler. Varsayılan kapalı. Besteciyi dizüstü bilgisayar ve standart masaüstü genişliklerinde düzenli tutmak için yalnızca etkinleştirildiğinde geniş ekranlarda (≥1400 piksel) görünür.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Konuşma ana hattını göster',
|
settings_label_conversation_outline: 'Konuşma ana hattını göster',
|
||||||
settings_desc_conversation_outline: 'Geçerli konuşmadaki kullanıcı soruları için yalnızca masaüstünde görünen bir atlama listesi gösterir. Varsayılan kapalıdır.',
|
settings_desc_conversation_outline: 'Geçerli konuşmadaki kullanıcı soruları için yalnızca masaüstünde görünen bir atlama listesi gösterir. Varsayılan kapalıdır.',
|
||||||
settings_label_hide_suggestions: 'Yeni sohbet önerilerini gizle',
|
settings_label_hide_suggestions: 'Yeni sohbet önerilerini gizle',
|
||||||
@@ -18978,6 +18989,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Göndermek için bir mesaj yazın',
|
composer_disabled_empty: 'Göndermek için bir mesaj yazın',
|
||||||
composer_mobile_workspace: 'Çalışma alanı',
|
composer_mobile_workspace: 'Çalışma alanı',
|
||||||
composer_mobile_model: 'Modeli',
|
composer_mobile_model: 'Modeli',
|
||||||
|
composer_mobile_quota: 'Kota',
|
||||||
composer_mobile_reasoning: 'muhakeme',
|
composer_mobile_reasoning: 'muhakeme',
|
||||||
composer_mobile_context: 'Bağlam',
|
composer_mobile_context: 'Bağlam',
|
||||||
|
|
||||||
@@ -19907,7 +19919,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Ponów próbę',
|
settings_autosave_retry: 'Ponów próbę',
|
||||||
settings_label_language: 'Język',
|
settings_label_language: 'Język',
|
||||||
settings_label_quota_chip: 'Pokaż wskaźnik limitu dostawcy w polu wprowadzania',
|
settings_label_quota_chip: 'Pokaż wskaźnik limitu dostawcy w polu wprowadzania',
|
||||||
settings_desc_quota_chip: 'Wyświetla subtelny wskaźnik pozostałego limitu (np. saldo kredytów OpenRouter) w stopce pola wprowadzania. Domyślnie wyłączone. Widoczne tylko na szerokich ekranach (≥1400px), aby utrzymać pole wprowadzania czytelnym na laptopach i standardowych monitorach.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Pokaż zarys konwersacji',
|
settings_label_conversation_outline: 'Pokaż zarys konwersacji',
|
||||||
settings_desc_conversation_outline: 'Pokazuje desktopową listę skrótów do pytań użytkownika w bieżącej konwersacji. Domyślnie wyłączone.',
|
settings_desc_conversation_outline: 'Pokazuje desktopową listę skrótów do pytań użytkownika w bieżącej konwersacji. Domyślnie wyłączone.',
|
||||||
settings_label_hide_suggestions: 'Ukryj sugestie nowej konwersacji',
|
settings_label_hide_suggestions: 'Ukryj sugestie nowej konwersacji',
|
||||||
@@ -20686,6 +20698,7 @@ const LOCALES = {
|
|||||||
composer_disabled_empty: 'Wpisz wiadomość, aby wysłać',
|
composer_disabled_empty: 'Wpisz wiadomość, aby wysłać',
|
||||||
composer_mobile_workspace: 'Obszar roboczy',
|
composer_mobile_workspace: 'Obszar roboczy',
|
||||||
composer_mobile_model: 'Model',
|
composer_mobile_model: 'Model',
|
||||||
|
composer_mobile_quota: 'Limit',
|
||||||
composer_mobile_reasoning: 'Myślenie',
|
composer_mobile_reasoning: 'Myślenie',
|
||||||
composer_mobile_context: 'Kontekst',
|
composer_mobile_context: 'Kontekst',
|
||||||
media_audio_label: 'Dźwięk',
|
media_audio_label: 'Dźwięk',
|
||||||
@@ -21380,7 +21393,7 @@ const LOCALES = {
|
|||||||
settings_autosave_retry: 'Thử lại',
|
settings_autosave_retry: 'Thử lại',
|
||||||
settings_label_language: 'Ngôn ngữ',
|
settings_label_language: 'Ngôn ngữ',
|
||||||
settings_label_quota_chip: 'Hiện hạn mức nhà cung cấp trong khung soạn thảo',
|
settings_label_quota_chip: 'Hiện hạn mức nhà cung cấp trong khung soạn thảo',
|
||||||
settings_desc_quota_chip: 'Hiển thị chỉ báo quota còn lại (ví dụ: số dư credit OpenRouter) ở footer của composer. Mặc định tắt. Chỉ hiển thị trên màn hình rộng (≥1400px) khi bật để giữ composer gọn gàng trên laptop và desktop thông thường.',
|
settings_desc_quota_chip: 'Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.',
|
||||||
settings_label_conversation_outline: 'Hiện mục lục cuộc trò chuyện',
|
settings_label_conversation_outline: 'Hiện mục lục cuộc trò chuyện',
|
||||||
settings_desc_conversation_outline: 'Hiển thị danh sách nhảy nhanh cho các câu hỏi của người dùng trong cuộc trò chuyện hiện tại trên desktop. Mặc định tắt.',
|
settings_desc_conversation_outline: 'Hiển thị danh sách nhảy nhanh cho các câu hỏi của người dùng trong cuộc trò chuyện hiện tại trên desktop. Mặc định tắt.',
|
||||||
settings_label_hide_suggestions: 'Ẩn gợi ý chat mới',
|
settings_label_hide_suggestions: 'Ẩn gợi ý chat mới',
|
||||||
@@ -22170,6 +22183,7 @@ const LOCALES = {
|
|||||||
composer_compression_will_queue: 'Nhập tin nhắn — hệ thống sẽ xếp hàng và gửi sau khi nén xong',
|
composer_compression_will_queue: 'Nhập tin nhắn — hệ thống sẽ xếp hàng và gửi sau khi nén xong',
|
||||||
composer_mobile_workspace: 'Workspace',
|
composer_mobile_workspace: 'Workspace',
|
||||||
composer_mobile_model: 'Model',
|
composer_mobile_model: 'Model',
|
||||||
|
composer_mobile_quota: 'Hạn mức',
|
||||||
composer_mobile_reasoning: 'Lập luận',
|
composer_mobile_reasoning: 'Lập luận',
|
||||||
composer_mobile_context: 'Ngữ cảnh',
|
composer_mobile_context: 'Ngữ cảnh',
|
||||||
context_block_name_default: 'Ngữ cảnh',
|
context_block_name_default: 'Ngữ cảnh',
|
||||||
|
|||||||
+7
-3
@@ -624,7 +624,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="icon-btn composer-mobile-config-btn" id="composerMobileConfigBtn" type="button" onclick="toggleMobileComposerConfig()" title="Workspace, model, reasoning, and context settings" aria-label="Workspace, model, reasoning, and context settings" aria-haspopup="true" aria-expanded="false" aria-controls="composerMobileConfigPanel">
|
<button class="icon-btn composer-mobile-config-btn" id="composerMobileConfigBtn" type="button" onclick="toggleMobileComposerConfig()" title="Workspace, model, quota, reasoning, and context settings" aria-label="Workspace, model, quota, reasoning, and context settings" aria-haspopup="true" aria-expanded="false" aria-controls="composerMobileConfigPanel">
|
||||||
<svg id="composerMobileCtxRing" viewBox="0 0 36 36" width="24" height="24" style="display:block;overflow:visible;transform:rotate(-90deg)" aria-hidden="true"><circle cx="18" cy="18" r="14" fill="none" stroke="currentColor" stroke-width="3.5" opacity="0.2"/><circle id="ctx-arc" cx="18" cy="18" r="14" fill="none" stroke="#22c55e" stroke-width="3.5" stroke-dasharray="87.96 87.96" stroke-dashoffset="87.96" stroke-linecap="round"/><text id="ctx-num" x="18" y="18" text-anchor="middle" dominant-baseline="middle" style="transform:rotate(90deg);transform-origin:18px 18px;font-size:9px;font-weight:700;fill:currentColor;stroke:none">0</text></svg>
|
<svg id="composerMobileCtxRing" viewBox="0 0 36 36" width="24" height="24" style="display:block;overflow:visible;transform:rotate(-90deg)" aria-hidden="true"><circle cx="18" cy="18" r="14" fill="none" stroke="currentColor" stroke-width="3.5" opacity="0.2"/><circle id="ctx-arc" cx="18" cy="18" r="14" fill="none" stroke="#22c55e" stroke-width="3.5" stroke-dasharray="87.96 87.96" stroke-dashoffset="87.96" stroke-linecap="round"/><text id="ctx-num" x="18" y="18" text-anchor="middle" dominant-baseline="middle" style="transform:rotate(90deg);transform-origin:18px 18px;font-size:9px;font-weight:700;fill:currentColor;stroke:none">0</text></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="composer-model-wrap">
|
<div class="composer-model-wrap">
|
||||||
@@ -703,7 +703,7 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="composer-mobile-config-panel" id="composerMobileConfigPanel" aria-label="Workspace, model, reasoning, and context settings">
|
<div class="composer-mobile-config-panel" id="composerMobileConfigPanel" aria-label="Workspace, model, quota, reasoning, and context settings">
|
||||||
<button class="composer-mobile-config-action" id="composerMobileWorkspaceAction" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace">
|
<button class="composer-mobile-config-action" id="composerMobileWorkspaceAction" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace">
|
||||||
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
|
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
|
||||||
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_workspace">Workspace</span><span class="composer-mobile-config-value" id="composerMobileWorkspaceLabel"></span></span>
|
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_workspace">Workspace</span><span class="composer-mobile-config-value" id="composerMobileWorkspaceLabel"></span></span>
|
||||||
@@ -712,6 +712,10 @@
|
|||||||
<span class="composer-model-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span>
|
<span class="composer-model-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span>
|
||||||
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_model">Model</span><span class="composer-mobile-config-value" id="composerMobileModelLabel"></span></span>
|
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_model">Model</span><span class="composer-mobile-config-value" id="composerMobileModelLabel"></span></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="composer-mobile-config-action" id="composerMobileQuotaAction" type="button" onclick="switchPanel('settings');switchSettingsSection('providers')" title="Provider quota" style="display:none">
|
||||||
|
<span class="composer-model-icon" aria-hidden="true"><span class="provider-quota-chip-dot" style="box-shadow:none"></span></span>
|
||||||
|
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_quota">Quota</span><span class="composer-mobile-config-value" id="composerMobileQuotaLabel"></span></span>
|
||||||
|
</button>
|
||||||
<button class="composer-mobile-config-action" id="composerMobileReasoningAction" type="button" onclick="toggleReasoningDropdown()" title="Reasoning effort level" style="display:none">
|
<button class="composer-mobile-config-action" id="composerMobileReasoningAction" type="button" onclick="toggleReasoningDropdown()" title="Reasoning effort level" style="display:none">
|
||||||
<span class="composer-reasoning-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.46 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.46 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"/></svg></span>
|
<span class="composer-reasoning-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.46 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.46 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"/></svg></span>
|
||||||
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_reasoning">Reasoning</span><span class="composer-mobile-config-value" id="composerMobileReasoningLabel"></span></span>
|
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_reasoning">Reasoning</span><span class="composer-mobile-config-value" id="composerMobileReasoningLabel"></span></span>
|
||||||
@@ -1233,7 +1237,7 @@
|
|||||||
<input type="checkbox" id="settingsShowQuotaChip" style="width:15px;height:15px;accent-color:var(--accent)">
|
<input type="checkbox" id="settingsShowQuotaChip" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||||
<span data-i18n="settings_label_quota_chip">Show provider quota chip in composer</span>
|
<span data-i18n="settings_label_quota_chip">Show provider quota chip in composer</span>
|
||||||
</label>
|
</label>
|
||||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_quota_chip">Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Default off. Only visible on wide displays (≥1400px) when enabled, to keep the composer uncluttered on laptop and standard desktop widths.</div>
|
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_quota_chip">Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Shown by default when provider quota data is available.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||||
|
|||||||
+7
-4
@@ -7377,7 +7377,10 @@ function _preferencesPayloadFromUi(){
|
|||||||
const showUsageCb=$('settingsShowTokenUsage');
|
const showUsageCb=$('settingsShowTokenUsage');
|
||||||
if(showUsageCb) payload.show_token_usage=showUsageCb.checked;
|
if(showUsageCb) payload.show_token_usage=showUsageCb.checked;
|
||||||
const showQuotaChipCb=$('settingsShowQuotaChip');
|
const showQuotaChipCb=$('settingsShowQuotaChip');
|
||||||
if(showQuotaChipCb) payload.show_quota_chip=showQuotaChipCb.checked;
|
if(showQuotaChipCb){
|
||||||
|
payload.show_quota_chip=showQuotaChipCb.checked;
|
||||||
|
payload.show_quota_chip_opt_out=!showQuotaChipCb.checked;
|
||||||
|
}
|
||||||
const showConversationOutlineCb=$('settingsShowConversationOutline');
|
const showConversationOutlineCb=$('settingsShowConversationOutline');
|
||||||
if(showConversationOutlineCb) payload.show_conversation_outline=showConversationOutlineCb.checked;
|
if(showConversationOutlineCb) payload.show_conversation_outline=showConversationOutlineCb.checked;
|
||||||
const hideSuggestionsCb=$('settingsHideSuggestions');
|
const hideSuggestionsCb=$('settingsHideSuggestions');
|
||||||
@@ -7812,11 +7815,10 @@ async function loadSettingsPanel(){
|
|||||||
maxTokensField.dataset.initialValue=maxTokensField.value;
|
maxTokensField.dataset.initialValue=maxTokensField.value;
|
||||||
maxTokensField.addEventListener('input',_markSettingsDirty,{once:false});
|
maxTokensField.addEventListener('input',_markSettingsDirty,{once:false});
|
||||||
}
|
}
|
||||||
// Ambient provider quota chip toggle — default off; only shows at ≥1400px viewport
|
// Ambient provider quota chip toggle — default on when provider quota data is available.
|
||||||
// when enabled (see style.css @media (max-width:1399.98px) rule).
|
|
||||||
const showQuotaChipCb=$('settingsShowQuotaChip');
|
const showQuotaChipCb=$('settingsShowQuotaChip');
|
||||||
if(showQuotaChipCb){
|
if(showQuotaChipCb){
|
||||||
showQuotaChipCb.checked=settings.show_quota_chip===true;
|
showQuotaChipCb.checked=settings.show_quota_chip!==false;
|
||||||
window._showQuotaChip=showQuotaChipCb.checked;
|
window._showQuotaChip=showQuotaChipCb.checked;
|
||||||
showQuotaChipCb.addEventListener('change',()=>{
|
showQuotaChipCb.addEventListener('change',()=>{
|
||||||
window._showQuotaChip=showQuotaChipCb.checked;
|
window._showQuotaChip=showQuotaChipCb.checked;
|
||||||
@@ -10557,6 +10559,7 @@ async function saveSettings(andClose){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
body.show_quota_chip=showQuotaChip===true;
|
body.show_quota_chip=showQuotaChip===true;
|
||||||
|
body.show_quota_chip_opt_out=showQuotaChip!==true;
|
||||||
body.show_conversation_outline=showConversationOutline===true;
|
body.show_conversation_outline=showConversationOutline===true;
|
||||||
body.show_tps=showTps;
|
body.show_tps=showTps;
|
||||||
body.fade_text_effect=fadeTextEffect;
|
body.fade_text_effect=fadeTextEffect;
|
||||||
|
|||||||
+38
-77
@@ -2397,12 +2397,13 @@
|
|||||||
.composer-workspace-chip.active{color:var(--text);background:var(--accent-bg);}
|
.composer-workspace-chip.active{color:var(--text);background:var(--accent-bg);}
|
||||||
.composer-workspace-icon,.composer-workspace-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
.composer-workspace-icon,.composer-workspace-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||||
.composer-workspace-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
.composer-workspace-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||||
.composer-reasoning-chip{display:inline-flex;align-items:center;gap:8px;max-width:180px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
|
.composer-reasoning-chip{display:inline-flex;align-items:center;gap:5px;max-width:none;padding:8px 10px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
|
||||||
.composer-reasoning-chip.inactive{opacity:.78;}
|
.composer-reasoning-chip.inactive{opacity:.78;}
|
||||||
.composer-reasoning-chip:hover{color:var(--text);background-color:var(--hover-bg);}
|
.composer-reasoning-chip:hover{color:var(--text);background-color:var(--hover-bg);}
|
||||||
.composer-reasoning-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
|
.composer-reasoning-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
|
||||||
.composer-reasoning-icon,.composer-reasoning-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
.composer-reasoning-icon,.composer-reasoning-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||||
.composer-reasoning-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
.composer-reasoning-chevron{display:none;}
|
||||||
|
.composer-reasoning-label{display:inline-flex;align-items:center;justify-content:center;min-width:1.6ch;font-size:11px;font-weight:700;letter-spacing:0;text-transform:none;}
|
||||||
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:140px;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;padding:4px;overflow:hidden;}
|
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:140px;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;padding:4px;overflow:hidden;}
|
||||||
.composer-reasoning-dropdown.open{display:block;}
|
.composer-reasoning-dropdown.open{display:block;}
|
||||||
.reasoning-option{padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .12s;}
|
.reasoning-option{padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .12s;}
|
||||||
@@ -2441,29 +2442,13 @@
|
|||||||
.provider-quota-chip:hover{background:rgba(34,197,94,.16);border-color:rgba(34,197,94,.35);}
|
.provider-quota-chip:hover{background:rgba(34,197,94,.16);border-color:rgba(34,197,94,.35);}
|
||||||
.provider-quota-chip-dot{width:6px;height:6px;border-radius:999px;background:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.12);flex:0 0 auto;}
|
.provider-quota-chip-dot{width:6px;height:6px;border-radius:999px;background:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.12);flex:0 0 auto;}
|
||||||
.provider-quota-chip-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
.provider-quota-chip-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||||
/* Hide the ambient quota chip below 1400px — the composer footer already
|
|
||||||
has profile + workspace + model picker + reasoning + media + send and
|
|
||||||
was getting cramped (model picker truncating from "Claude Sonnet 4 7"
|
|
||||||
to "Claude Sonnet 4", workspace dropping more text) once the chip
|
|
||||||
squeezed in. Mobile users get provider info via the dedicated
|
|
||||||
mobile-config drawer; laptop desktop users can find quota in
|
|
||||||
Settings → Providers (the chip's onclick target). Show only on wide
|
|
||||||
desktop where there's genuine room. Aron's standing position on
|
|
||||||
composer real estate (PR #1721, May 13 2026) reinforces this.
|
|
||||||
|
|
||||||
Using 1399.98px rather than 1400px to avoid the one-pixel boundary
|
|
||||||
overlap with the existing @media (min-width:1400px) `.messages-inner`
|
|
||||||
rule at line 893 — at exactly 1400px both would fire, so the chip
|
|
||||||
was hidden AT the wide-desktop boundary where it should first appear. */
|
|
||||||
@media (max-width:1399.98px){
|
|
||||||
.provider-quota-chip{display:none!important;}
|
|
||||||
}
|
|
||||||
/* Increased specificity (.icon-btn.composer-mobile-config-btn) to win the cascade
|
/* Increased specificity (.icon-btn.composer-mobile-config-btn) to win the cascade
|
||||||
against the later-in-source `.icon-btn { display:flex }` rule that this element
|
against the later-in-source `.icon-btn { display:flex }` rule that this element
|
||||||
also matches. Without this, the button shows at desktop width — see #1381 review. */
|
also matches. Without this, the button shows at desktop width — see #1381 review. */
|
||||||
.icon-btn.composer-mobile-config-btn{display:none;}
|
.icon-btn.composer-mobile-config-btn{display:none;}
|
||||||
.composer-mobile-config-btn.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--text);opacity:1;}
|
.composer-mobile-config-btn.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--text);opacity:1;}
|
||||||
.composer-mobile-config-panel{display:none;position:absolute;left:8px;right:8px;bottom:calc(100% + 6px);z-index:180;padding:8px;gap:8px;align-items:stretch;justify-content:flex-start;flex-wrap:wrap;background:var(--surface);border:1px solid var(--border2);border-radius:12px;box-shadow:0 -6px 28px rgba(0,0,0,.35);overflow:visible;}
|
.composer-mobile-config-panel{display:none;position:absolute;left:8px;right:8px;bottom:calc(100% + 6px);z-index:180;padding:8px;gap:8px;align-items:stretch;justify-content:flex-start;flex-wrap:wrap;background:var(--surface);border:1px solid var(--border2);border-radius:12px;box-shadow:0 -6px 28px rgba(0,0,0,.35);overflow:visible;}
|
||||||
|
.composer-mobile-config-panel.open{display:flex;}
|
||||||
.composer-mobile-config-action{box-sizing:border-box;display:inline-flex;align-items:center;gap:10px;min-width:0;min-height:44px;padding:8px 10px;border-radius:10px;border:1px solid transparent;background:transparent;color:var(--muted);font:inherit;text-align:left;cursor:pointer;overflow:hidden;}
|
.composer-mobile-config-action{box-sizing:border-box;display:inline-flex;align-items:center;gap:10px;min-width:0;min-height:44px;padding:8px 10px;border-radius:10px;border:1px solid transparent;background:transparent;color:var(--muted);font:inherit;text-align:left;cursor:pointer;overflow:hidden;}
|
||||||
.composer-mobile-config-action.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--text);}
|
.composer-mobile-config-action.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--text);}
|
||||||
.composer-mobile-config-copy{display:flex;flex-direction:column;gap:1px;min-width:0;line-height:1.2;}
|
.composer-mobile-config-copy{display:flex;flex-direction:column;gap:1px;min-width:0;line-height:1.2;}
|
||||||
@@ -2769,67 +2754,43 @@
|
|||||||
.workspace-panel-edge-toggle{display:none!important;}
|
.workspace-panel-edge-toggle{display:none!important;}
|
||||||
.rightpanel{display:none}
|
.rightpanel{display:none}
|
||||||
.workspace-toggle-btn,.mobile-files-btn{display:inline-flex!important;}
|
.workspace-toggle-btn,.mobile-files-btn{display:inline-flex!important;}
|
||||||
|
.app-titlebar{justify-content:space-between;}
|
||||||
|
.app-titlebar-hamburger,.app-titlebar-spacer{display:flex;}
|
||||||
}
|
}
|
||||||
|
|
||||||
@container composer-footer (max-width: 700px){
|
/* Fit-based composer footer collapse. ui.js measures actual overflow on
|
||||||
/* Stage 1: remove long workspace/model text first to avoid clipping.
|
.composer-left and toggles these stage classes on .composer-footer. */
|
||||||
The left sidebar + panel layout can consume width before right panel opens, so this
|
.composer-footer.cf-icons .composer-left{gap:10px;overflow-x:auto;overflow-y:hidden;}
|
||||||
must be container-driven and independent of panel state. */
|
.composer-footer.cf-icons .composer-left > *{flex-shrink:0;}
|
||||||
.composer-workspace-label,
|
.composer-footer.cf-icons .composer-profile-wrap,
|
||||||
#composerWorkspaceLabel,
|
.composer-footer.cf-icons .composer-ws-wrap{flex:0 0 auto;}
|
||||||
.composer-model-label,
|
.composer-footer.cf-icons .composer-profile-label,
|
||||||
.composer-model-chevron,
|
.composer-footer.cf-icons .composer-profile-chevron,
|
||||||
#composerModelLabel{display:none;}
|
.composer-footer.cf-icons #composerProfileLabel{display:none;}
|
||||||
|
.composer-footer.cf-icons .composer-profile-chip{box-sizing:border-box;width:44px;height:44px;max-width:44px;min-width:44px;min-height:44px;padding:0;justify-content:center;gap:0;font-size:11px;}
|
||||||
|
.composer-footer.cf-icons .composer-workspace-label,
|
||||||
|
.composer-footer.cf-icons #composerWorkspaceLabel{display:none;}
|
||||||
|
.composer-footer.cf-icons .composer-workspace-chip{padding-left:8px;padding-right:8px;gap:2px;}
|
||||||
|
.composer-footer.cf-icons .composer-model-label,
|
||||||
|
.composer-footer.cf-icons #composerModelLabel{display:none;}
|
||||||
|
.composer-footer.cf-icons .composer-model-chip{max-width:none;padding-left:10px;padding-right:8px;gap:4px;}
|
||||||
|
.composer-footer.cf-icons #btnSavedPrompts,
|
||||||
|
.composer-footer.cf-icons .saved-prompts-popup{display:none!important;}
|
||||||
|
.composer-footer.cf-icons .composer-divider{display:none;}
|
||||||
|
|
||||||
.composer-workspace-chip,
|
.composer-footer.cf-burger .composer-left{gap:8px;}
|
||||||
.composer-model-chip{max-width:52px;min-width:44px;min-height:44px;padding:6px;justify-content:center;gap:0;font-size:11px;}
|
.composer-footer.cf-burger .composer-workspace-group{box-sizing:border-box;flex:0 0 44px;width:44px;min-width:44px;max-width:44px;min-height:44px;overflow:hidden;border:none;background:transparent;border-radius:8px;}
|
||||||
.composer-workspace-group{min-height:44px;}
|
.composer-footer.cf-burger .composer-workspace-files-btn{box-sizing:border-box;width:44px;min-width:44px;max-width:44px;padding:0;border-radius:8px;}
|
||||||
.composer-workspace-files-btn{min-width:44px;padding:6px 8px;}
|
.composer-footer.cf-burger .composer-workspace-chip{display:none!important;}
|
||||||
.composer-divider{display:none;}
|
.composer-footer.cf-burger .composer-left > .composer-model-wrap,
|
||||||
.ctx-indicator-wrap{display:none!important;}
|
.composer-footer.cf-burger .provider-quota-chip,
|
||||||
.ctx-indicator-wrap{display:none!important;}
|
.composer-footer.cf-burger .composer-left > .composer-reasoning-wrap,
|
||||||
.provider-quota-chip{display:none!important;}
|
.composer-footer.cf-burger .composer-left > .composer-toolsets-wrap{display:none!important;}
|
||||||
}
|
.composer-footer.cf-burger .composer-mobile-config-btn{box-sizing:border-box;position:relative;display:inline-flex!important;width:44px;height:44px;min-width:44px;min-height:44px;flex-shrink:0;}
|
||||||
|
.composer-footer.cf-burger .composer-mobile-config-panel.open{display:flex;}
|
||||||
@container composer-footer (max-width: 520px){
|
.composer-footer.cf-burger .composer-mobile-config-action{flex:1 1 0;min-height:44px;min-width:0;background:var(--input-bg);border-color:var(--border);}
|
||||||
/* Stage 2: full icon-only for tighter widths. Container-width compaction is needed
|
.composer-footer.cf-burger .composer-mobile-context-action{flex:1 0 100%;}
|
||||||
when side panels narrow the composer even though the viewport is not a phone. */
|
.composer-footer.cf-burger .ctx-indicator-wrap{display:none!important;}
|
||||||
.composer-left{gap:10px;overflow-x:auto;overflow-y:hidden;}
|
|
||||||
.composer-left > *{flex-shrink:0;}
|
|
||||||
.composer-profile-wrap,
|
|
||||||
.composer-ws-wrap{flex:0 0 auto;}
|
|
||||||
.composer-profile-label,
|
|
||||||
.composer-workspace-label,
|
|
||||||
.composer-model-label,
|
|
||||||
.composer-reasoning-label,
|
|
||||||
.composer-profile-chevron,
|
|
||||||
.composer-workspace-chevron,
|
|
||||||
.composer-model-chevron,
|
|
||||||
.composer-reasoning-chevron,
|
|
||||||
#composerProfileLabel,
|
|
||||||
#composerWorkspaceLabel,
|
|
||||||
#composerModelLabel,
|
|
||||||
#composerReasoningLabel{display:none;}
|
|
||||||
|
|
||||||
.composer-profile-chip,
|
|
||||||
.composer-model-chip,
|
|
||||||
.composer-reasoning-chip{box-sizing:border-box;width:44px;height:44px;max-width:44px;min-width:44px;min-height:44px;padding:0;justify-content:center;gap:0;font-size:11px;}
|
|
||||||
|
|
||||||
.composer-workspace-group{box-sizing:border-box;flex:0 0 44px;width:44px;min-width:44px;max-width:44px;min-height:44px;overflow:hidden;border:none;background:transparent;border-radius:8px;}
|
|
||||||
.composer-workspace-files-btn{box-sizing:border-box;width:44px;min-width:44px;max-width:44px;padding:0;border-radius:8px;}
|
|
||||||
.composer-workspace-chip{display:none!important;}
|
|
||||||
.composer-left > .composer-model-wrap,
|
|
||||||
.composer-left > .composer-reasoning-wrap,
|
|
||||||
.composer-left > .composer-toolsets-wrap{display:none!important;}
|
|
||||||
/* Saved prompts is a desktop-only composer affordance — too much for the
|
|
||||||
narrow mobile composer (Nathan 2026-06-09). Hide the button + popup. */
|
|
||||||
#btnSavedPrompts,.saved-prompts-popup{display:none!important;}
|
|
||||||
.composer-mobile-config-btn{box-sizing:border-box;position:relative;display:inline-flex!important;width:44px;height:44px;min-width:44px;min-height:44px;flex-shrink:0;}
|
|
||||||
.composer-mobile-config-panel.open{display:flex;}
|
|
||||||
.composer-mobile-config-action{flex:1 1 0;min-height:44px;min-width:0;background:var(--input-bg);border-color:var(--border);}
|
|
||||||
.composer-mobile-context-action{flex:1 0 100%;}
|
|
||||||
.composer-divider{display:none;}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show session-toolsets chip ONLY when the composer-footer container is wide
|
/* Show session-toolsets chip ONLY when the composer-footer container is wide
|
||||||
enough to comfortably accommodate it (workspace+model+reasoning+profile+
|
enough to comfortably accommodate it (workspace+model+reasoning+profile+
|
||||||
|
|||||||
+165
-24
@@ -1624,7 +1624,7 @@ function _providerQuotaIndicatorText(status){
|
|||||||
if(accountLimits&&Array.isArray(accountLimits.windows)&&accountLimits.windows.length){
|
if(accountLimits&&Array.isArray(accountLimits.windows)&&accountLimits.windows.length){
|
||||||
const w=accountLimits.windows.find(x=>x&&Number.isFinite(Number(x.remaining_percent)))||accountLimits.windows[0];
|
const w=accountLimits.windows.find(x=>x&&Number.isFinite(Number(x.remaining_percent)))||accountLimits.windows[0];
|
||||||
const remaining=_formatQuotaPercentShort(w&&w.remaining_percent);
|
const remaining=_formatQuotaPercentShort(w&&w.remaining_percent);
|
||||||
if(remaining) return {label:provider+' '+remaining, title:(status.message||'Provider usage loaded')+' — '+remaining+' remaining'};
|
if(remaining) return {label:remaining, title:provider+' — '+(status.message||'Provider usage loaded')+' — '+remaining+' remaining'};
|
||||||
}
|
}
|
||||||
const quota=status.quota||null;
|
const quota=status.quota||null;
|
||||||
if(quota){
|
if(quota){
|
||||||
@@ -1635,7 +1635,7 @@ function _providerQuotaIndicatorText(status){
|
|||||||
const parts=[];
|
const parts=[];
|
||||||
if(used) parts.push('used '+used);
|
if(used) parts.push('used '+used);
|
||||||
if(limit) parts.push('limit '+limit);
|
if(limit) parts.push('limit '+limit);
|
||||||
return {label:provider+' '+remaining, title:(status.message||'Provider quota loaded')+(parts.length?' — '+parts.join(' · '):'')};
|
return {label:remaining, title:provider+' — '+(status.message||'Provider quota loaded')+(parts.length?' — '+parts.join(' · '):'')};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -1643,14 +1643,17 @@ function _providerQuotaIndicatorText(status){
|
|||||||
function renderProviderQuotaIndicator(status){
|
function renderProviderQuotaIndicator(status){
|
||||||
const chip=$('providerQuotaChip');
|
const chip=$('providerQuotaChip');
|
||||||
const label=$('providerQuotaChipLabel');
|
const label=$('providerQuotaChipLabel');
|
||||||
|
const mobileAction=$('composerMobileQuotaAction');
|
||||||
|
const mobileLabel=$('composerMobileQuotaLabel');
|
||||||
if(!chip||!label) return;
|
if(!chip||!label) return;
|
||||||
// Hide entirely when the user has disabled the ambient quota chip in Settings.
|
// Hide entirely when the user has disabled the ambient quota chip in Settings.
|
||||||
// Default is off (window._showQuotaChip defaults to false in boot.js) so users
|
// Boot defaults this on; an explicit false preference suppresses it.
|
||||||
// never see the chip unless they opt in.
|
|
||||||
if(window._showQuotaChip!==true){
|
if(window._showQuotaChip!==true){
|
||||||
chip.hidden=true;
|
chip.hidden=true;
|
||||||
label.textContent='';
|
label.textContent='';
|
||||||
chip.removeAttribute('title');
|
chip.removeAttribute('title');
|
||||||
|
if(mobileAction){mobileAction.style.display='none';mobileAction.removeAttribute('title');}
|
||||||
|
if(mobileLabel) mobileLabel.textContent='';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text=_providerQuotaIndicatorText(status);
|
const text=_providerQuotaIndicatorText(status);
|
||||||
@@ -1658,11 +1661,15 @@ function renderProviderQuotaIndicator(status){
|
|||||||
chip.hidden=true;
|
chip.hidden=true;
|
||||||
label.textContent='';
|
label.textContent='';
|
||||||
chip.removeAttribute('title');
|
chip.removeAttribute('title');
|
||||||
|
if(mobileAction){mobileAction.style.display='none';mobileAction.removeAttribute('title');}
|
||||||
|
if(mobileLabel) mobileLabel.textContent='';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
label.textContent=text.label;
|
label.textContent=text.label;
|
||||||
chip.title=text.title;
|
chip.title=text.title;
|
||||||
chip.hidden=false;
|
chip.hidden=false;
|
||||||
|
if(mobileAction){mobileAction.style.display='';mobileAction.title=text.title;}
|
||||||
|
if(mobileLabel) mobileLabel.textContent=text.label;
|
||||||
}
|
}
|
||||||
async function refreshProviderQuotaIndicator(){
|
async function refreshProviderQuotaIndicator(){
|
||||||
// Short-circuit before the fetch when the chip is disabled — no point asking
|
// Short-circuit before the fetch when the chip is disabled — no point asking
|
||||||
@@ -1670,6 +1677,10 @@ async function refreshProviderQuotaIndicator(){
|
|||||||
if(window._showQuotaChip!==true){
|
if(window._showQuotaChip!==true){
|
||||||
const chip=$('providerQuotaChip');
|
const chip=$('providerQuotaChip');
|
||||||
if(chip){chip.hidden=true;chip.removeAttribute('title');}
|
if(chip){chip.hidden=true;chip.removeAttribute('title');}
|
||||||
|
const mobileAction=$('composerMobileQuotaAction');
|
||||||
|
if(mobileAction){mobileAction.style.display='none';mobileAction.removeAttribute('title');}
|
||||||
|
const mobileLabel=$('composerMobileQuotaLabel');
|
||||||
|
if(mobileLabel) mobileLabel.textContent='';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(_providerQuotaRefreshInFlight) return;
|
if(_providerQuotaRefreshInFlight) return;
|
||||||
@@ -2385,6 +2396,37 @@ function _getConfiguredModelBadge(modelId,badgeMap,providerId){
|
|||||||
return matches[0];
|
return matches[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _compactComposerModelChipLabel(modelId,labelText){
|
||||||
|
const id=String(modelId||'').trim();
|
||||||
|
const raw=String(labelText||'').trim();
|
||||||
|
if(!raw) return getModelLabel(id);
|
||||||
|
const idLower=id.toLowerCase();
|
||||||
|
const rawLower=raw.toLowerCase();
|
||||||
|
const slash=id.indexOf('/');
|
||||||
|
if(slash>0){
|
||||||
|
const provider=id.slice(0,slash).toLowerCase();
|
||||||
|
if(rawLower.startsWith(provider+'/')){
|
||||||
|
return raw.slice(provider.length+1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(id&&rawLower===idLower&&raw.includes('/')){
|
||||||
|
return raw.slice(raw.indexOf('/')+1).trim();
|
||||||
|
}
|
||||||
|
if(raw.includes('/') && !/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)){
|
||||||
|
const parts=raw.split('/').map(s=>s.trim()).filter(Boolean);
|
||||||
|
if(parts.length>=2){
|
||||||
|
const tail=parts[parts.length-1];
|
||||||
|
const tailLower=tail.toLowerCase();
|
||||||
|
if(idLower && (tailLower===idLower || idLower.endsWith('/'+tailLower))) return tail;
|
||||||
|
if(parts.length===2){
|
||||||
|
const leadLower=parts[0].toLowerCase();
|
||||||
|
if(tailLower.startsWith(leadLower+'-')) return tail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
function syncModelChip(){
|
function syncModelChip(){
|
||||||
const sel=$('modelSelect');
|
const sel=$('modelSelect');
|
||||||
const chip=$('composerModelChip');
|
const chip=$('composerModelChip');
|
||||||
@@ -2402,8 +2444,9 @@ function syncModelChip(){
|
|||||||
}
|
}
|
||||||
const opt=_selectedModelOption();
|
const opt=_selectedModelOption();
|
||||||
const text=opt?opt.textContent:getModelLabel(sel.value||'');
|
const text=opt?opt.textContent:getModelLabel(sel.value||'');
|
||||||
|
const compactText=_compactComposerModelChipLabel(sel.value||'', text);
|
||||||
const gatewayRouting=_latestGatewayRoutingForSession(S.session);
|
const gatewayRouting=_latestGatewayRoutingForSession(S.session);
|
||||||
const displayText=_formatGatewayModelLabel(sel.value||'',text,gatewayRouting)||text;
|
const displayText=_formatGatewayModelLabel(sel.value||'',compactText,gatewayRouting)||compactText;
|
||||||
label.textContent=displayText;
|
label.textContent=displayText;
|
||||||
if(mobileLabel) mobileLabel.textContent=displayText;
|
if(mobileLabel) mobileLabel.textContent=displayText;
|
||||||
chip.title=gatewayRouting?`${sel.value||'Conversation model'} ${_gatewayRoutingLabel(gatewayRouting)}`:(sel.value||'Conversation model');
|
chip.title=gatewayRouting?`${sel.value||'Conversation model'} ${_gatewayRoutingLabel(gatewayRouting)}`:(sel.value||'Conversation model');
|
||||||
@@ -3220,6 +3263,79 @@ window.addEventListener('resize',()=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Fit-based composer footer collapse ──────────────────────────────────────
|
||||||
|
// Stage classes on .composer-footer:
|
||||||
|
// (none) full labels · .cf-icons icon chips · .cf-icons.cf-burger hamburger.
|
||||||
|
let _composerFitScheduled=false;
|
||||||
|
let _composerFitResizeObserver=null;
|
||||||
|
let _composerFitMutationObserver=null;
|
||||||
|
let _composerFitObservedFooter=null;
|
||||||
|
let _composerFitResizeListenerBound=false;
|
||||||
|
|
||||||
|
function _fitComposerFooter(){
|
||||||
|
const footer=document.querySelector('.composer-footer');
|
||||||
|
if(!footer) return;
|
||||||
|
const left=footer.querySelector('.composer-left');
|
||||||
|
if(!left) return;
|
||||||
|
if(!left.clientWidth) return;
|
||||||
|
const overflows=function(){return left.scrollWidth>left.clientWidth+1;};
|
||||||
|
footer.classList.remove('cf-icons','cf-burger');
|
||||||
|
if(!overflows()) return;
|
||||||
|
footer.classList.add('cf-icons');
|
||||||
|
if(!overflows()) return;
|
||||||
|
footer.classList.add('cf-burger');
|
||||||
|
}
|
||||||
|
window._fitComposerFooter=_fitComposerFooter;
|
||||||
|
|
||||||
|
function _scheduleComposerFit(){
|
||||||
|
if(_composerFitScheduled) return;
|
||||||
|
_composerFitScheduled=true;
|
||||||
|
requestAnimationFrame(function(){
|
||||||
|
_composerFitScheduled=false;
|
||||||
|
try{_fitComposerFooter();}catch(_){ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window._scheduleComposerFit=_scheduleComposerFit;
|
||||||
|
|
||||||
|
function _initComposerFooterFit(){
|
||||||
|
const footer=document.querySelector('.composer-footer');
|
||||||
|
const left=footer&&footer.querySelector('.composer-left');
|
||||||
|
if(!footer||!left) return;
|
||||||
|
_scheduleComposerFit();
|
||||||
|
if(_composerFitObservedFooter===footer) return;
|
||||||
|
if(_composerFitResizeObserver){try{_composerFitResizeObserver.disconnect();}catch(_){ }}
|
||||||
|
if(_composerFitMutationObserver){try{_composerFitMutationObserver.disconnect();}catch(_){ }}
|
||||||
|
_composerFitResizeObserver=null;
|
||||||
|
_composerFitMutationObserver=null;
|
||||||
|
_composerFitObservedFooter=footer;
|
||||||
|
if(window.ResizeObserver){
|
||||||
|
try{
|
||||||
|
_composerFitResizeObserver=new ResizeObserver(_scheduleComposerFit);
|
||||||
|
_composerFitResizeObserver.observe(footer);
|
||||||
|
}catch(_){ }
|
||||||
|
}
|
||||||
|
if(window.MutationObserver){
|
||||||
|
try{
|
||||||
|
_composerFitMutationObserver=new MutationObserver(_scheduleComposerFit);
|
||||||
|
_composerFitMutationObserver.observe(left,{
|
||||||
|
childList:true,subtree:true,characterData:true,
|
||||||
|
attributes:true,attributeFilter:['class','style','hidden']
|
||||||
|
});
|
||||||
|
}catch(_){ }
|
||||||
|
}
|
||||||
|
if(!_composerFitResizeListenerBound){
|
||||||
|
window.addEventListener('resize',_scheduleComposerFit);
|
||||||
|
_composerFitResizeListenerBound=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window._initComposerFooterFit=_initComposerFooterFit;
|
||||||
|
|
||||||
|
if(document.readyState==='loading'){
|
||||||
|
document.addEventListener('DOMContentLoaded',_initComposerFooterFit);
|
||||||
|
}else{
|
||||||
|
_initComposerFooterFit();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Reasoning effort chip ────────────────────────────────────────────────────
|
// ── Reasoning effort chip ────────────────────────────────────────────────────
|
||||||
let _currentReasoningEffort=null;
|
let _currentReasoningEffort=null;
|
||||||
let _currentReasoningEffortsSupported=null;
|
let _currentReasoningEffortsSupported=null;
|
||||||
@@ -3231,7 +3347,12 @@ function _normalizeReasoningEffort(eff){
|
|||||||
function _formatReasoningEffortLabel(effort){
|
function _formatReasoningEffortLabel(effort){
|
||||||
if(effort==='none') return 'None';
|
if(effort==='none') return 'None';
|
||||||
if(!effort) return 'Default';
|
if(!effort) return 'Default';
|
||||||
return effort;
|
if(effort==='minimal') return 'Minimal';
|
||||||
|
if(effort==='low') return 'Low';
|
||||||
|
if(effort==='medium') return 'Medium';
|
||||||
|
if(effort==='high') return 'High';
|
||||||
|
if(effort==='xhigh') return 'XHigh';
|
||||||
|
return effort.charAt(0).toUpperCase()+effort.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _reasoningEffortContext(){
|
function _reasoningEffortContext(){
|
||||||
@@ -3304,7 +3425,9 @@ function _applyReasoningChip(eff){
|
|||||||
if(chip){
|
if(chip){
|
||||||
const inactive=!effort||effort==='none';
|
const inactive=!effort||effort==='none';
|
||||||
chip.classList.toggle('inactive',inactive);
|
chip.classList.toggle('inactive',inactive);
|
||||||
chip.title='Reasoning effort: '+text;
|
const labelText='Reasoning effort: '+text;
|
||||||
|
chip.title=labelText;
|
||||||
|
chip.setAttribute('aria-label',labelText);
|
||||||
}
|
}
|
||||||
if(mobileAction) mobileAction.classList.toggle('inactive',!effort||effort==='none');
|
if(mobileAction) mobileAction.classList.toggle('inactive',!effort||effort==='none');
|
||||||
_highlightReasoningOption(effort);
|
_highlightReasoningOption(effort);
|
||||||
@@ -3790,6 +3913,18 @@ function closeMobileComposerConfig(){
|
|||||||
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openMobileComposerConfig(){
|
||||||
|
const panel=$('composerMobileConfigPanel');
|
||||||
|
if(!panel) return;
|
||||||
|
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
|
||||||
|
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
||||||
|
closeModelDropdown();
|
||||||
|
closeReasoningDropdown();
|
||||||
|
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
|
||||||
|
panel.classList.add('open');
|
||||||
|
_syncMobileComposerConfigButton(true);
|
||||||
|
}
|
||||||
|
|
||||||
function toggleMobileComposerConfig(){
|
function toggleMobileComposerConfig(){
|
||||||
const panel=$('composerMobileConfigPanel');
|
const panel=$('composerMobileConfigPanel');
|
||||||
if(!panel) return;
|
if(!panel) return;
|
||||||
@@ -3801,15 +3936,23 @@ function toggleMobileComposerConfig(){
|
|||||||
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
|
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
|
openMobileComposerConfig();
|
||||||
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
|
||||||
closeModelDropdown();
|
|
||||||
closeReasoningDropdown();
|
|
||||||
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
|
|
||||||
panel.classList.add('open');
|
|
||||||
_syncMobileComposerConfigButton(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openComposerContextMenu(e){
|
||||||
|
if(e){
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
const tooltip=$('ctxTooltip');
|
||||||
|
if(tooltip){
|
||||||
|
tooltip.classList.remove('ctx-tooltip-active');
|
||||||
|
tooltip.setAttribute('aria-hidden','true');
|
||||||
|
}
|
||||||
|
openMobileComposerConfig();
|
||||||
|
}
|
||||||
|
window.openComposerContextMenu=openComposerContextMenu;
|
||||||
|
|
||||||
document.addEventListener('click',function(e){
|
document.addEventListener('click',function(e){
|
||||||
if(
|
if(
|
||||||
e.target.closest('#composerMobileConfigBtn') ||
|
e.target.closest('#composerMobileConfigBtn') ||
|
||||||
@@ -4469,7 +4612,7 @@ function _clearActivityElapsedTimer(){
|
|||||||
_activityElapsedTimerGroup=null;
|
_activityElapsedTimerGroup=null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _MOBILE_CONFIG_BASE_LABEL='Workspace, model, reasoning, and context settings';
|
const _MOBILE_CONFIG_BASE_LABEL='Workspace, model, quota, reasoning, and context settings';
|
||||||
|
|
||||||
function _setCtxCompressButton(btn,text){
|
function _setCtxCompressButton(btn,text){
|
||||||
if(!btn)return;
|
if(!btn)return;
|
||||||
@@ -4699,20 +4842,16 @@ function _syncCtxIndicator(usage){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Touch support: toggle context tooltip on tap (#524) ──
|
// ── Touch support: toggle context tooltip on tap (#524) ──
|
||||||
// On mobile, hover doesn't work — allow tap on the context ring button
|
// Hover/focus still exposes the compact tooltip, but a click/tap now opens the
|
||||||
// to toggle the tooltip visibility so the compress affordance is reachable.
|
// shared composer config menu used by the phone footer so the richer context
|
||||||
|
// details and compress action have one interaction path.
|
||||||
document.addEventListener('DOMContentLoaded',function(){
|
document.addEventListener('DOMContentLoaded',function(){
|
||||||
const wrap=document.getElementById('ctxIndicatorWrap');
|
const wrap=document.getElementById('ctxIndicatorWrap');
|
||||||
const tooltip=document.getElementById('ctxTooltip');
|
const tooltip=document.getElementById('ctxTooltip');
|
||||||
if(!wrap||!tooltip)return;
|
if(!wrap||!tooltip)return;
|
||||||
const btn=document.getElementById('ctxIndicator');
|
const btn=document.getElementById('ctxIndicator');
|
||||||
if(!btn)return;
|
if(!btn)return;
|
||||||
btn.addEventListener('click',function(e){
|
btn.addEventListener('click',openComposerContextMenu);
|
||||||
e.stopPropagation();
|
|
||||||
const isOpen=tooltip.classList.contains('ctx-tooltip-active');
|
|
||||||
tooltip.classList.toggle('ctx-tooltip-active',!isOpen);
|
|
||||||
tooltip.setAttribute('aria-hidden',String(isOpen));
|
|
||||||
});
|
|
||||||
// Close on outside tap
|
// Close on outside tap
|
||||||
document.addEventListener('click',function(){
|
document.addEventListener('click',function(){
|
||||||
tooltip.classList.remove('ctx-tooltip-active');
|
tooltip.classList.remove('ctx-tooltip-active');
|
||||||
@@ -5002,7 +5141,9 @@ function _gatewayRoutingLabel(routing){
|
|||||||
function _formatGatewayModelLabel(modelId,labelText,routing){
|
function _formatGatewayModelLabel(modelId,labelText,routing){
|
||||||
if(!routing)return'';
|
if(!routing)return'';
|
||||||
const usedModel=String(routing.used_model||'').trim();
|
const usedModel=String(routing.used_model||'').trim();
|
||||||
const base=usedModel?getModelLabel(usedModel):(labelText||getModelLabel(modelId));
|
const base=usedModel
|
||||||
|
?_compactComposerModelChipLabel(usedModel,getModelLabel(usedModel))
|
||||||
|
:_compactComposerModelChipLabel(modelId,labelText||getModelLabel(modelId));
|
||||||
const via=_gatewayRoutingLabel(routing);
|
const via=_gatewayRoutingLabel(routing);
|
||||||
return via?`${base} ${via}`:base;
|
return via?`${base} ${via}`:base;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
The chip must:
|
The chip must:
|
||||||
* Be hidden by default (CSS base rule).
|
* Be hidden by default (CSS base rule).
|
||||||
* Be shown only at wide composer-footer widths (>= 1100px container query).
|
* Be shown only at wide composer-footer widths (>= 1100px container query).
|
||||||
* Stay hidden on mobile (@media max-width:640px and @container 520px).
|
* Stay hidden on mobile (@media max-width:640px and the .cf-burger stage).
|
||||||
* Have its visibility controlled by CSS, NOT by JS (single source of truth).
|
* Have its visibility controlled by CSS, NOT by JS (single source of truth).
|
||||||
* Continue to track state through _applyToolsetsChip() so /api/session/toolsets
|
* Continue to track state through _applyToolsetsChip() so /api/session/toolsets
|
||||||
keeps working for scripted callers regardless of UI visibility.
|
keeps working for scripted callers regardless of UI visibility.
|
||||||
@@ -12,7 +12,7 @@ import re
|
|||||||
|
|
||||||
|
|
||||||
def _src(name: str) -> str:
|
def _src(name: str) -> str:
|
||||||
with open(f"static/{name}") as f:
|
with open(f"static/{name}", encoding="utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
@@ -47,16 +47,15 @@ class TestToolsetsChipResponsiveCSS:
|
|||||||
"that shows .composer-toolsets-wrap with display:block or display:flex"
|
"that shows .composer-toolsets-wrap with display:block or display:flex"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_narrow_container_query_keeps_hiding(self):
|
def test_burger_stage_keeps_hiding(self):
|
||||||
"""The existing @container (max-width: 520px) rule must still hide the chip."""
|
"""The fit-engine hamburger stage must still hide the chip."""
|
||||||
css = _src("style.css")
|
css = _src("style.css")
|
||||||
# Look for the existing 520px rule that already hid composer-toolsets-wrap
|
|
||||||
m = re.search(
|
m = re.search(
|
||||||
r'@container\s+composer-footer\s*\(\s*max-width:\s*520px\s*\).*?\.composer-toolsets-wrap\s*\{\s*display:\s*none\s*!important',
|
r'\.composer-footer\.cf-burger\b.*?\.composer-toolsets-wrap\s*\{\s*display:\s*none\s*!important',
|
||||||
css, re.DOTALL,
|
css, re.DOTALL,
|
||||||
)
|
)
|
||||||
assert m, (
|
assert m, (
|
||||||
"@container composer-footer (max-width: 520px) must continue to "
|
".composer-footer.cf-burger must continue to "
|
||||||
"hide .composer-toolsets-wrap with !important"
|
"hide .composer-toolsets-wrap with !important"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ class TestToolsetsAPIStillWorks:
|
|||||||
"""The api/session/toolsets endpoint must still be registered."""
|
"""The api/session/toolsets endpoint must still be registered."""
|
||||||
# Check api/routes.py for the endpoint
|
# Check api/routes.py for the endpoint
|
||||||
try:
|
try:
|
||||||
with open("api/routes.py") as f:
|
with open("api/routes.py", encoding="utf-8") as f:
|
||||||
src = f.read()
|
src = f.read()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# If routes.py is named differently, search
|
# If routes.py is named differently, search
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ const fs = require('fs');
|
|||||||
const src = fs.readFileSync(process.argv[2], 'utf8');
|
const src = fs.readFileSync(process.argv[2], 'utf8');
|
||||||
|
|
||||||
function makeEl() {
|
function makeEl() {
|
||||||
|
const attrs = {};
|
||||||
return {
|
return {
|
||||||
style: {}, dataset: {}, title: '', textContent: '', value: '',
|
style: {}, dataset: {}, title: '', textContent: '', value: '',
|
||||||
classList: { add(){}, remove(){}, toggle(){}, contains(){return false} },
|
classList: { add(){}, remove(){}, toggle(){}, contains(){return false} },
|
||||||
|
setAttribute(name, value){ attrs[name] = String(value); },
|
||||||
|
getAttribute(name){ return Object.prototype.hasOwnProperty.call(attrs, name) ? attrs[name] : null; },
|
||||||
|
removeAttribute(name){ delete attrs[name]; },
|
||||||
querySelectorAll(){return []}, querySelector(){return null},
|
querySelectorAll(){return []}, querySelector(){return null},
|
||||||
getBoundingClientRect(){return {left:0,top:0,width:0,height:0}},
|
getBoundingClientRect(){return {left:0,top:0,width:0,height:0}},
|
||||||
};
|
};
|
||||||
|
|||||||
+128
-73
@@ -351,92 +351,89 @@ def _container_media_block(css: str, media_query: str):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def test_composer_controls_switch_to_icon_only_by_container_width():
|
def test_composer_controls_switch_to_fit_stage_classes():
|
||||||
"""Composer controls should progressively compact based on footer width."""
|
"""Composer controls should progressively compact based on measured overflow."""
|
||||||
assert re.search(r'\.composer-footer\s*\{[^}]*container-type:inline-size[^}]*container-name:composer-footer[^}]*\}', CSS), \
|
assert re.search(r'\.composer-footer\s*\{[^}]*container-type:inline-size[^}]*container-name:composer-footer[^}]*\}', CSS), \
|
||||||
".composer-footer should define container-type:inline-size and container-name:composer-footer"
|
".composer-footer should define container-type:inline-size and container-name:composer-footer"
|
||||||
compact_700 = _container_query_block(CSS, "composer-footer (max-width: 700px)")
|
assert "@container composer-footer (max-width: 700px)" not in CSS
|
||||||
assert compact_700, "Expected composer mid-width compact rules at @container composer-footer (max-width: 700px)"
|
assert "@container composer-footer (max-width: 520px)" not in CSS
|
||||||
for selector in (
|
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
".composer-workspace-label",
|
fit_body = _js_function_body(ui_js, "_fitComposerFooter")
|
||||||
".composer-model-label",
|
assert "scrollWidth" in fit_body and "clientWidth" in fit_body, \
|
||||||
".composer-model-chevron",
|
"_fitComposerFooter() should measure actual left-cluster overflow"
|
||||||
"#composerWorkspaceLabel",
|
assert "cf-icons" in fit_body and "cf-burger" in fit_body, \
|
||||||
"#composerModelLabel",
|
"_fitComposerFooter() should toggle the compact stage classes"
|
||||||
".composer-workspace-chip",
|
|
||||||
".composer-model-chip",
|
|
||||||
".composer-divider",
|
|
||||||
):
|
|
||||||
assert selector in compact_700, f"{selector} should be present in the 700px composer compact block"
|
|
||||||
assert "display:none" in compact_700
|
|
||||||
assert "max-width:52px" in compact_700
|
|
||||||
# Ensure this first stage does not prematurely remove profile/reasoning labels.
|
|
||||||
assert ".composer-profile-label" not in compact_700
|
|
||||||
assert ".composer-reasoning-label" not in compact_700
|
|
||||||
assert ".composer-profile-chevron" not in compact_700
|
|
||||||
assert ".composer-reasoning-chevron" not in compact_700
|
|
||||||
|
|
||||||
compact_520 = _container_query_block(CSS, "composer-footer (max-width: 520px)")
|
|
||||||
assert compact_520, "Expected full composer icon-only rules at @container composer-footer (max-width: 520px)"
|
|
||||||
for selector in (
|
for selector in (
|
||||||
".composer-profile-label",
|
".composer-footer.cf-icons .composer-profile-label",
|
||||||
".composer-workspace-label",
|
".composer-footer.cf-icons .composer-profile-chevron",
|
||||||
".composer-model-label",
|
".composer-footer.cf-icons #composerProfileLabel",
|
||||||
".composer-reasoning-label",
|
".composer-footer.cf-icons .composer-workspace-label",
|
||||||
".composer-profile-chevron",
|
".composer-footer.cf-icons #composerWorkspaceLabel",
|
||||||
".composer-workspace-chevron",
|
".composer-footer.cf-icons .composer-model-label",
|
||||||
".composer-model-chevron",
|
".composer-footer.cf-icons #composerModelLabel",
|
||||||
".composer-reasoning-chevron",
|
".composer-footer.cf-icons .composer-profile-chip",
|
||||||
"#composerProfileLabel",
|
".composer-footer.cf-icons .composer-model-chip",
|
||||||
"#composerWorkspaceLabel",
|
".composer-footer.cf-icons .composer-divider",
|
||||||
"#composerModelLabel",
|
|
||||||
"#composerReasoningLabel",
|
|
||||||
".composer-model-chip",
|
|
||||||
".composer-profile-chip",
|
|
||||||
".composer-reasoning-chip",
|
|
||||||
):
|
):
|
||||||
assert selector in compact_520, f"{selector} should be present in the 520px composer compact block"
|
assert selector in CSS, f"{selector} should be present in the .cf-icons rules"
|
||||||
assert "width:44px" in compact_520
|
assert ".composer-footer.cf-icons .composer-profile-chip{box-sizing:border-box;width:44px" in CSS, \
|
||||||
assert "display:none" in compact_520
|
".cf-icons should collapse the profile chip to an icon-sized control"
|
||||||
assert ".composer-workspace-chip{display:none!important" in compact_520.replace(" ", ""), \
|
assert ".composer-footer.cf-icons .composer-workspace-chip{display:none!important" not in CSS.replace(" ", ""), \
|
||||||
"520px container compact mode must remove the blank workspace switch slot"
|
".cf-icons should keep the workspace switch visible rather than blanking it"
|
||||||
assert ".composer-left>*{flex-shrink:0" in compact_520.replace(" ", ""), \
|
|
||||||
"520px container compact mode must stop controls from shrinking into each other"
|
for selector in (
|
||||||
assert ".composer-mobile-config-btn" in compact_520 and "display:inline-flex!important" in compact_520, \
|
".composer-footer.cf-burger .composer-workspace-group",
|
||||||
"520px container compact mode must expose the mobile config button even when viewport is wider than 640px"
|
".composer-footer.cf-burger .composer-workspace-files-btn",
|
||||||
|
".composer-footer.cf-burger .composer-workspace-chip",
|
||||||
|
".composer-footer.cf-burger .composer-left > .composer-model-wrap",
|
||||||
|
".composer-footer.cf-burger .provider-quota-chip",
|
||||||
|
".composer-footer.cf-burger .composer-left > .composer-reasoning-wrap",
|
||||||
|
".composer-footer.cf-burger .composer-left > .composer-toolsets-wrap",
|
||||||
|
".composer-footer.cf-burger .composer-mobile-config-btn",
|
||||||
|
".composer-footer.cf-burger .composer-mobile-config-panel.open",
|
||||||
|
".composer-footer.cf-burger .composer-mobile-context-action",
|
||||||
|
".composer-footer.cf-burger .ctx-indicator-wrap",
|
||||||
|
):
|
||||||
|
assert selector in CSS, f"{selector} should be present in the .cf-burger rules"
|
||||||
|
assert ".composer-footer.cf-burger .composer-workspace-chip{display:none!important".replace(" ", "") in CSS.replace(" ", ""), \
|
||||||
|
".cf-burger must remove the blank workspace switch slot"
|
||||||
|
assert ".composer-footer.cf-burger .provider-quota-chip" in CSS and ".composer-footer.cf-burger .composer-left > .composer-toolsets-wrap{display:none!important" in CSS, \
|
||||||
|
".cf-burger must fold the inline quota chip into the shared config menu"
|
||||||
|
assert ".composer-footer.cf-burger .composer-mobile-config-btn{box-sizing:border-box;position:relative;display:inline-flex!important" in CSS, \
|
||||||
|
".cf-burger must expose the config button even on wider viewports"
|
||||||
|
|
||||||
# Regression intent:
|
# Regression intent:
|
||||||
# - this container rule should not depend on right-panel open/closed state.
|
# - this measured rule should not depend on right-panel open/closed state.
|
||||||
# - left-sidebar-only constriction must still collapse composer controls together.
|
# - left-sidebar-only constriction must still collapse composer controls together.
|
||||||
assert ".layout:not(.workspace-panel-collapsed)" not in compact_700, \
|
assert ".layout:not(.workspace-panel-collapsed)" not in CSS, \
|
||||||
"composer-footer compact rule should be state-agnostic (left sidebar + closed right panel case included)"
|
"composer-footer compact stages should be state-agnostic"
|
||||||
assert ".layout:not(.workspace-panel-collapsed)" not in compact_520, \
|
|
||||||
"composer-footer compact rule should be state-agnostic (left sidebar + closed right panel case included)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_composer_700px_workspace_switch_does_not_become_blank_chip():
|
def test_composer_icon_stage_workspace_switch_does_not_become_blank_chip():
|
||||||
"""The 700px container state may hide the workspace label, but needs a switch affordance."""
|
"""The icon stage may hide workspace text, but keeps a visible switch affordance."""
|
||||||
compact_700 = _container_query_block(CSS, "composer-footer (max-width: 700px)")
|
css_ns = CSS.replace(" ", "")
|
||||||
assert compact_700, "Expected composer mid-width compact rules at @container composer-footer (max-width: 700px)"
|
assert ".composer-footer.cf-icons .composer-workspace-label" in CSS
|
||||||
|
assert ".composer-footer.cf-icons #composerWorkspaceLabel" in CSS
|
||||||
workspace_label = _declarations(_rule_body(compact_700, ".composer-workspace-label"))
|
assert ".composer-footer.cf-icons .composer-workspace-chip{display:none!important" not in css_ns, \
|
||||||
workspace_chip = _optional_declarations(compact_700, ".composer-workspace-chip")
|
".cf-icons must not hide the visible workspace switch chip"
|
||||||
workspace_chevron = _optional_declarations(compact_700, ".composer-workspace-chevron")
|
assert ".composer-footer.cf-icons .composer-workspace-chevron{display:none" not in css_ns, \
|
||||||
mobile_config = _optional_declarations(compact_700, ".composer-mobile-config-btn")
|
".cf-icons must not leave the visible workspace switch chip without its chevron"
|
||||||
|
|
||||||
assert _display_hidden(workspace_label), \
|
|
||||||
"700px container state should hide the long workspace label before tighter mobile rules"
|
|
||||||
if not _display_hidden(workspace_chip) and not _display_inline_flex(mobile_config):
|
|
||||||
assert not _display_hidden(workspace_chevron), \
|
|
||||||
"700px container state must not leave the visible workspace switch chip without a label or chevron"
|
|
||||||
|
|
||||||
|
|
||||||
def test_composer_compact_switch_is_not_viewport_only():
|
def test_composer_compact_switch_is_not_viewport_only():
|
||||||
"""Compact controls should be container-triggered, not bound to viewport width alone."""
|
"""Compact controls should be content-triggered, not bound to viewport width alone."""
|
||||||
assert "composer-footer (max-width: 700px)" in CSS, \
|
assert ".composer-footer.cf-icons" in CSS, \
|
||||||
"Container-query breakpoint should track composer footer width"
|
"Icon-chip stage should be expressed as the .cf-icons class"
|
||||||
assert "composer-footer (max-width: 520px)" in CSS, \
|
assert ".composer-footer.cf-burger" in CSS, \
|
||||||
"Container-query second-stage breakpoint should track composer footer width"
|
"Hamburger stage should be expressed as the .cf-burger class"
|
||||||
|
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
|
assert "_fitComposerFooter" in ui_js and "scrollWidth" in ui_js, \
|
||||||
|
"Composer compacting should be driven by measured overflow"
|
||||||
|
assert "ResizeObserver" in ui_js and "MutationObserver" in ui_js, \
|
||||||
|
"Composer fitting should rerun when footer size or chip content changes"
|
||||||
|
assert "composer-footer (max-width: 700px)" not in CSS
|
||||||
|
assert "composer-footer (max-width: 520px)" not in CSS
|
||||||
assert re.search(r'@container\s+composer-footer\s*\(max-width:\s*860px\)', CSS) is None, \
|
assert re.search(r'@container\s+composer-footer\s*\(max-width:\s*860px\)', CSS) is None, \
|
||||||
"Full icon-only should not be tied to a 860px threshold any more"
|
"Full icon-only should not be tied to a 860px threshold any more"
|
||||||
assert re.search(r'@container\s+composer-footer\s*\(max-width:\s*1000px\)', CSS) is None, \
|
assert re.search(r'@container\s+composer-footer\s*\(max-width:\s*1000px\)', CSS) is None, \
|
||||||
@@ -576,6 +573,17 @@ def test_mobile_sidebar_opens_as_full_screen_surface_with_panel_rail():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compact_titlebar_keeps_hamburger_available():
|
||||||
|
"""Compact app chrome must keep the titlebar menu reachable."""
|
||||||
|
compact_css = "\n".join(_max_width_media_blocks(900))
|
||||||
|
assert re.search(r'\.app-titlebar-hamburger,\s*\.app-titlebar-spacer\{[^}]*display:\s*flex', compact_css), (
|
||||||
|
"Compact titlebar should expose the hamburger before true phone width"
|
||||||
|
)
|
||||||
|
assert ".rightpanel{display:none}" in compact_css.replace(" ", ""), (
|
||||||
|
"The compact titlebar breakpoint should match the hidden workspace-panel breakpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_rail_click_opens_full_screen_panel_drawer():
|
def test_mobile_rail_click_opens_full_screen_panel_drawer():
|
||||||
"""Rail clicks on phone should keep the full-screen drawer open for panel switching."""
|
"""Rail clicks on phone should keep the full-screen drawer open for panel switching."""
|
||||||
panels_js = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
|
panels_js = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
|
||||||
@@ -1217,6 +1225,8 @@ def test_model_and_reasoning_controls_live_in_mobile_overflow_panel():
|
|||||||
panel_html = HTML[panel_start:panel_end]
|
panel_html = HTML[panel_start:panel_end]
|
||||||
assert 'id="composerMobileModelAction"' in panel_html, \
|
assert 'id="composerMobileModelAction"' in panel_html, \
|
||||||
"mobile model action must be inside the overflow panel"
|
"mobile model action must be inside the overflow panel"
|
||||||
|
assert 'id="composerMobileQuotaAction"' in panel_html, \
|
||||||
|
"mobile quota action must be inside the overflow panel"
|
||||||
assert 'id="composerMobileReasoningAction"' in panel_html, \
|
assert 'id="composerMobileReasoningAction"' in panel_html, \
|
||||||
"mobile reasoning action must be inside the overflow panel"
|
"mobile reasoning action must be inside the overflow panel"
|
||||||
assert 'onclick="toggleModelDropdown()"' in panel_html, \
|
assert 'onclick="toggleModelDropdown()"' in panel_html, \
|
||||||
@@ -1225,11 +1235,15 @@ def test_model_and_reasoning_controls_live_in_mobile_overflow_panel():
|
|||||||
"mobile reasoning action must reuse the existing reasoning dropdown"
|
"mobile reasoning action must reuse the existing reasoning dropdown"
|
||||||
assert 'id="composerMobileModelLabel"' in panel_html, \
|
assert 'id="composerMobileModelLabel"' in panel_html, \
|
||||||
"mobile model action must expose the selected model label"
|
"mobile model action must expose the selected model label"
|
||||||
|
assert 'id="composerMobileQuotaLabel"' in panel_html, \
|
||||||
|
"mobile quota action must expose the selected quota label"
|
||||||
assert 'id="composerMobileReasoningLabel"' in panel_html, \
|
assert 'id="composerMobileReasoningLabel"' in panel_html, \
|
||||||
"mobile reasoning action must expose the selected reasoning label"
|
"mobile reasoning action must expose the selected reasoning label"
|
||||||
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
assert "composerMobileModelAction" in ui_js, \
|
assert "composerMobileModelAction" in ui_js, \
|
||||||
"model dropdown positioning/click handling must know the mobile model action"
|
"model dropdown positioning/click handling must know the mobile model action"
|
||||||
|
assert "composerMobileQuotaAction" in ui_js, \
|
||||||
|
"quota sync must know the mobile quota action"
|
||||||
assert "composerMobileReasoningAction" in ui_js, \
|
assert "composerMobileReasoningAction" in ui_js, \
|
||||||
"reasoning dropdown positioning/click handling must know the mobile reasoning action"
|
"reasoning dropdown positioning/click handling must know the mobile reasoning action"
|
||||||
|
|
||||||
@@ -1242,6 +1256,20 @@ def test_model_and_reasoning_controls_live_in_mobile_overflow_panel():
|
|||||||
"mobile overflow panel must size the model/reasoning actions"
|
"mobile overflow panel must size the model/reasoning actions"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_overflow_panel_quota_order_matches_desktop_sequence():
|
||||||
|
"""The mobile overflow panel should keep the same shared control order as desktop."""
|
||||||
|
panel_start = HTML.index('id="composerMobileConfigPanel"')
|
||||||
|
panel_end = HTML.index('<div class="profile-dropdown"', panel_start)
|
||||||
|
panel_html = HTML[panel_start:panel_end]
|
||||||
|
workspace_idx = panel_html.index('id="composerMobileWorkspaceAction"')
|
||||||
|
model_idx = panel_html.index('id="composerMobileModelAction"')
|
||||||
|
quota_idx = panel_html.index('id="composerMobileQuotaAction"')
|
||||||
|
reasoning_idx = panel_html.index('id="composerMobileReasoningAction"')
|
||||||
|
context_idx = panel_html.index('id="composerMobileContextAction"')
|
||||||
|
assert workspace_idx < model_idx < quota_idx < reasoning_idx < context_idx, \
|
||||||
|
"mobile control order should mirror the desktop/shared control sequence"
|
||||||
|
|
||||||
|
|
||||||
def test_model_and_reasoning_dropdowns_use_mobile_panel_anchors():
|
def test_model_and_reasoning_dropdowns_use_mobile_panel_anchors():
|
||||||
"""Model/reasoning dropdowns must anchor to mobile actions while the overflow is open."""
|
"""Model/reasoning dropdowns must anchor to mobile actions while the overflow is open."""
|
||||||
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
@@ -1322,6 +1350,32 @@ def test_context_details_live_in_mobile_overflow_panel():
|
|||||||
"mobile compress affordance should be compact inside the context row"
|
"mobile compress affordance should be compact inside the context row"
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_indicator_click_opens_shared_mobile_config_menu():
|
||||||
|
"""The desktop context ring should open the same menu used by phone mode."""
|
||||||
|
ui_js = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
|
assert "function openMobileComposerConfig()" in ui_js, \
|
||||||
|
"mobile config open path should be reusable outside the phone button"
|
||||||
|
assert "function openComposerContextMenu(e)" in ui_js, \
|
||||||
|
"context indicator needs a named click path into the shared config menu"
|
||||||
|
|
||||||
|
context_menu_body = _js_function_body(ui_js, "openComposerContextMenu")
|
||||||
|
for expected in (
|
||||||
|
"e.preventDefault()",
|
||||||
|
"e.stopPropagation()",
|
||||||
|
"ctxTooltip",
|
||||||
|
"openMobileComposerConfig()",
|
||||||
|
):
|
||||||
|
assert expected in context_menu_body, \
|
||||||
|
f"context click should open the shared menu without leaving tooltip state behind ({expected})"
|
||||||
|
|
||||||
|
assert "btn.addEventListener('click',openComposerContextMenu)" in ui_js, \
|
||||||
|
"context indicator click must open the shared composer config menu"
|
||||||
|
|
||||||
|
panel_open = _declarations(_rule_body(CSS, ".composer-mobile-config-panel.open"))
|
||||||
|
assert panel_open.get("display") == "flex", \
|
||||||
|
"the shared composer config panel must be displayable when opened outside phone CSS"
|
||||||
|
|
||||||
|
|
||||||
def test_workspace_control_lives_in_mobile_overflow_panel():
|
def test_workspace_control_lives_in_mobile_overflow_panel():
|
||||||
"""Workspace switching must stay reachable even when the inline switch chip is hidden."""
|
"""Workspace switching must stay reachable even when the inline switch chip is hidden."""
|
||||||
panel_start = HTML.index('id="composerMobileConfigPanel"')
|
panel_start = HTML.index('id="composerMobileConfigPanel"')
|
||||||
@@ -1415,6 +1469,7 @@ def test_mobile_config_kickers_have_i18n_fallbacks():
|
|||||||
for key, label in (
|
for key, label in (
|
||||||
("composer_mobile_workspace", "Workspace"),
|
("composer_mobile_workspace", "Workspace"),
|
||||||
("composer_mobile_model", "Model"),
|
("composer_mobile_model", "Model"),
|
||||||
|
("composer_mobile_quota", "Quota"),
|
||||||
("composer_mobile_reasoning", "Reasoning"),
|
("composer_mobile_reasoning", "Reasoning"),
|
||||||
("composer_mobile_context", "Context"),
|
("composer_mobile_context", "Context"),
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Regression test for #show-quota-chip-toggle — Settings toggle to opt into the ambient quota chip.
|
"""Regression test for #show-quota-chip-toggle — Settings toggle for the ambient quota chip.
|
||||||
|
|
||||||
Quota chip default state is now OFF (per Nathan's directive 2026-05-16, immediately
|
Quota chip default state is ON so provider quota is visible when available, while
|
||||||
after the stage-371 release of #2082). Users opt in via Settings → Preferences.
|
users can still opt out via Settings -> Preferences.
|
||||||
"""
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -21,11 +21,13 @@ def test_quota_chip_settings_field_present():
|
|||||||
assert 'data-i18n="settings_desc_quota_chip"' in html
|
assert 'data-i18n="settings_desc_quota_chip"' in html
|
||||||
|
|
||||||
|
|
||||||
def test_quota_chip_default_off_in_config_defaults():
|
def test_quota_chip_default_on_in_config_defaults():
|
||||||
src = CONFIG.read_text(encoding="utf-8")
|
src = CONFIG.read_text(encoding="utf-8")
|
||||||
assert '"show_quota_chip": False' in src, "show_quota_chip must default to False (opt-in)"
|
assert '"show_quota_chip": True' in src, "show_quota_chip must default to True"
|
||||||
# Must be in the writable settings allow-list (bool keys)
|
# Must be in the writable settings allow-list (bool keys)
|
||||||
assert '"show_quota_chip",' in src, "show_quota_chip must be in _SETTINGS_BOOL_KEYS"
|
assert '"show_quota_chip",' in src, "show_quota_chip must be in _SETTINGS_BOOL_KEYS"
|
||||||
|
assert '"show_quota_chip_opt_out": False' in src, "opt-out marker must default to False"
|
||||||
|
assert '"show_quota_chip_opt_out",' in src, "opt-out marker must be in _SETTINGS_BOOL_KEYS"
|
||||||
|
|
||||||
|
|
||||||
def test_quota_chip_render_short_circuits_when_disabled():
|
def test_quota_chip_render_short_circuits_when_disabled():
|
||||||
@@ -48,6 +50,8 @@ def test_quota_chip_render_short_circuits_when_disabled():
|
|||||||
assert guard_idx < text_call_idx, (
|
assert guard_idx < text_call_idx, (
|
||||||
"Disabled-chip guard must run before the indicator-text computation"
|
"Disabled-chip guard must run before the indicator-text computation"
|
||||||
)
|
)
|
||||||
|
assert "composerMobileQuotaAction" in render_body
|
||||||
|
assert "composerMobileQuotaLabel" in render_body
|
||||||
|
|
||||||
# Refresher must short-circuit fetch when disabled
|
# Refresher must short-circuit fetch when disabled
|
||||||
refresh_start = js.index("async function refreshProviderQuotaIndicator(){")
|
refresh_start = js.index("async function refreshProviderQuotaIndicator(){")
|
||||||
@@ -57,17 +61,19 @@ def test_quota_chip_render_short_circuits_when_disabled():
|
|||||||
assert "window._showQuotaChip!==true" in refresh_head, (
|
assert "window._showQuotaChip!==true" in refresh_head, (
|
||||||
"refreshProviderQuotaIndicator must skip the fetch when chip is disabled"
|
"refreshProviderQuotaIndicator must skip the fetch when chip is disabled"
|
||||||
)
|
)
|
||||||
|
assert "composerMobileQuotaAction" in refresh_head
|
||||||
|
assert "composerMobileQuotaLabel" in refresh_head
|
||||||
|
|
||||||
|
|
||||||
def test_quota_chip_boot_initializes_default_off():
|
def test_quota_chip_boot_initializes_default_on():
|
||||||
js = BOOT.read_text(encoding="utf-8")
|
js = BOOT.read_text(encoding="utf-8")
|
||||||
# Both success path (reads from settings) and failure path (defaults block)
|
# Both success path (reads from settings) and failure path (defaults block)
|
||||||
# must set window._showQuotaChip
|
# must set window._showQuotaChip
|
||||||
assert "window._showQuotaChip=s.show_quota_chip===true" in js, (
|
assert "window._showQuotaChip=s.show_quota_chip!==false" in js, (
|
||||||
"Boot must initialize _showQuotaChip from settings.show_quota_chip"
|
"Boot must initialize _showQuotaChip from settings.show_quota_chip, defaulting on"
|
||||||
)
|
)
|
||||||
assert "window._showQuotaChip=false" in js, (
|
assert "window._showQuotaChip=true" in js, (
|
||||||
"Boot must default _showQuotaChip to false in the settings-fetch-failed branch"
|
"Boot must default _showQuotaChip to true in the settings-fetch-failed branch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,10 +82,12 @@ def test_quota_chip_panels_round_trip():
|
|||||||
# Payload read
|
# Payload read
|
||||||
assert "const showQuotaChipCb=$('settingsShowQuotaChip');" in js
|
assert "const showQuotaChipCb=$('settingsShowQuotaChip');" in js
|
||||||
assert "payload.show_quota_chip=showQuotaChipCb.checked;" in js
|
assert "payload.show_quota_chip=showQuotaChipCb.checked;" in js
|
||||||
|
assert "payload.show_quota_chip_opt_out=!showQuotaChipCb.checked;" in js
|
||||||
# Body assignment
|
# Body assignment
|
||||||
assert "body.show_quota_chip=showQuotaChip===true;" in js
|
assert "body.show_quota_chip=showQuotaChip===true;" in js
|
||||||
|
assert "body.show_quota_chip_opt_out=showQuotaChip!==true;" in js
|
||||||
# Settings panel load — checkbox is initialized from saved settings
|
# Settings panel load — checkbox is initialized from saved settings
|
||||||
assert "showQuotaChipCb.checked=settings.show_quota_chip===true;" in js
|
assert "showQuotaChipCb.checked=settings.show_quota_chip!==false;" in js
|
||||||
# Window-state propagation
|
# Window-state propagation
|
||||||
assert "window._showQuotaChip=showQuotaChip===true;" in js
|
assert "window._showQuotaChip=showQuotaChip===true;" in js
|
||||||
# Live refresh on toggle (immediate visual feedback)
|
# Live refresh on toggle (immediate visual feedback)
|
||||||
@@ -90,3 +98,35 @@ def test_quota_chip_localized_in_all_locales():
|
|||||||
js = I18N.read_text(encoding="utf-8")
|
js = I18N.read_text(encoding="utf-8")
|
||||||
assert js.count("settings_label_quota_chip:") == 14, "12 locales expected"
|
assert js.count("settings_label_quota_chip:") == 14, "12 locales expected"
|
||||||
assert js.count("settings_desc_quota_chip:") == 14, "12 locales expected"
|
assert js.count("settings_desc_quota_chip:") == 14, "12 locales expected"
|
||||||
|
|
||||||
|
|
||||||
|
def test_quota_chip_migrates_old_persisted_false_without_opt_out(tmp_path, monkeypatch):
|
||||||
|
import json
|
||||||
|
import api.config as config
|
||||||
|
|
||||||
|
settings_file = tmp_path / "settings.json"
|
||||||
|
settings_file.write_text(
|
||||||
|
json.dumps({"onboarding_completed": True, "show_quota_chip": False}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(config, "SETTINGS_FILE", settings_file)
|
||||||
|
|
||||||
|
assert config.load_settings()["show_quota_chip"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_quota_chip_honors_explicit_post_flip_opt_out(tmp_path, monkeypatch):
|
||||||
|
import json
|
||||||
|
import api.config as config
|
||||||
|
|
||||||
|
settings_file = tmp_path / "settings.json"
|
||||||
|
settings_file.write_text(
|
||||||
|
json.dumps({
|
||||||
|
"onboarding_completed": True,
|
||||||
|
"show_quota_chip": False,
|
||||||
|
"show_quota_chip_opt_out": True,
|
||||||
|
}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(config, "SETTINGS_FILE", settings_file)
|
||||||
|
|
||||||
|
assert config.load_settings()["show_quota_chip"] is False
|
||||||
@@ -29,8 +29,11 @@ const fs = require('fs');
|
|||||||
const src = fs.readFileSync(process.argv[2], 'utf8');
|
const src = fs.readFileSync(process.argv[2], 'utf8');
|
||||||
|
|
||||||
function makeEl() {
|
function makeEl() {
|
||||||
return {
|
return {
|
||||||
style: {},
|
style: {},
|
||||||
|
_attrs: {},
|
||||||
|
setAttribute(k, v){this._attrs[k] = String(v)},
|
||||||
|
getAttribute(k){return this._attrs[k]},
|
||||||
classList: {
|
classList: {
|
||||||
_set: new Set(),
|
_set: new Set(),
|
||||||
add(c){this._set.add(c)},
|
add(c){this._set.add(c)},
|
||||||
@@ -149,12 +152,13 @@ class TestChipAlwaysVisible:
|
|||||||
def test_low_shows_chip_active(self, driver_path):
|
def test_low_shows_chip_active(self, driver_path):
|
||||||
out = _apply(driver_path, "low")
|
out = _apply(driver_path, "low")
|
||||||
assert out["display"] == ""
|
assert out["display"] == ""
|
||||||
assert out["label"] == "low"
|
assert out["label"] == "Low"
|
||||||
assert out["inactive"] is False
|
assert out["inactive"] is False
|
||||||
|
|
||||||
def test_high_shows_chip_active(self, driver_path):
|
def test_high_shows_chip_active(self, driver_path):
|
||||||
out = _apply(driver_path, "high")
|
out = _apply(driver_path, "high")
|
||||||
assert out["display"] == ""
|
assert out["display"] == ""
|
||||||
|
assert out["label"] == "High"
|
||||||
assert out["inactive"] is False
|
assert out["inactive"] is False
|
||||||
|
|
||||||
|
|
||||||
@@ -178,7 +182,7 @@ class TestNormalizationEdgeCases:
|
|||||||
# Defensive: unknown effort still shows the chip rather than hiding.
|
# Defensive: unknown effort still shows the chip rather than hiding.
|
||||||
out = _apply(driver_path, "banana")
|
out = _apply(driver_path, "banana")
|
||||||
assert out["display"] == ""
|
assert out["display"] == ""
|
||||||
assert out["label"] == "banana"
|
assert out["label"] == "Banana"
|
||||||
assert out["inactive"] is False
|
assert out["inactive"] is False
|
||||||
|
|
||||||
|
|
||||||
@@ -196,4 +200,4 @@ class TestTitleAttributeAccessibility:
|
|||||||
|
|
||||||
def test_title_has_active_label_for_high(self, driver_path):
|
def test_title_has_active_label_for_high(self, driver_path):
|
||||||
out = _apply(driver_path, "high")
|
out = _apply(driver_path, "high")
|
||||||
assert out["title"] == "Reasoning effort: high"
|
assert out["title"] == "Reasoning effort: High"
|
||||||
Reference in new issue
Block a user