Skip to content

Commit ba0537d

Browse files
refactor(ui): шапка — отдельный composable
ChatOverlay.kt 1341 -> 1192 строк: строка статуса с иконками, полоса контекста и расход дневной квоты ушли в chat/ChatHeader.kt. Шесть почти одинаковых блоков Icon (зазор, размер, круг, подсветка открытой панели, clickable, внутренний отступ) заменены одним HeaderIcon: правка размера иконки теперь в одном месте. «Новая сессия» осталась отдельной — у неё combinedClickable и нет подсветки. PANEL_* и testTag-метки полос переехали в ChatHeader.kt (internal): их читают и сама шапка, и вызовы панелей в ChatOverlay. Параметры разделены по смыслу: ChatHeaderState (что показать) и ChatHeaderActions (переключить панель, диагностика, новая сессия, очистка) вместо двух десятков отдельных аргументов. Проверено на устройстве: геометрия шапки совпала до пикселя (Online 158, пинг 242, «4 MCP» 423, гейдж 107 на 626; иконки на 698/790/880/972/1062/1190 при y=219), все четыре панели открываются и закрываются по тапу, «Диагностика» открывает оверлей, long-press «+» даёт диалог «Очистить все сессии?».
1 parent 0250f7c commit ba0537d

3 files changed

Lines changed: 311 additions & 185 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,24 @@ ChatOverlay разобран на четыре файла (25.10.2026)
513513
распознавание (на телефоне вернулся отказ Yandex — сервис недоступен, это не код),
514514
набор текста убирает микрофон и возвращает его после очистки.
515515

516+
Шапка — отдельный composable (26.10.2026)
517+
- `ui/ChatOverlay.kt` — 1341 → 1192 строк: строка статуса с иконками, полоса
518+
контекста и расход дневной квоты ушли в `chat/ChatHeader.kt`.
519+
- Шесть почти одинаковых блоков `Icon` (зазор, размер, круг, подсветка открытой
520+
панели, `clickable`, внутренний отступ) заменены одним `HeaderIcon`: правка
521+
размера иконки теперь в одном месте, а не в шести. «Новая сессия» осталась
522+
отдельной — у неё `combinedClickable` и нет подсветки.
523+
- `PANEL_*` и testTag-метки полос переехали в `ChatHeader.kt` (`internal`): их
524+
читают и сама шапка, и вызовы панелей в `ChatOverlay`.
525+
- Параметры разделены по смыслу: `ChatHeaderState` (что показать) и
526+
`ChatHeaderActions` (переключить панель, диагностика, новая сессия, очистка) —
527+
вместо двух десятков отдельных аргументов. Внутри — четыре функции по группам:
528+
индикаторы, оформление, служебные иконки, полосы.
529+
- Проверено на устройстве: геометрия шапки совпала до пикселя (Online 158, пинг
530+
242, «4 MCP» 423, гейдж `107` на 626; иконки на 698/790/880/972/1062/1190 при
531+
y=219), все четыре панели открываются и закрываются по тапу, «Диагностика»
532+
открывает оверлей, long-press «+» даёт диалог «Очистить все сессии?».
533+
516534
---
517535

518536
## Стек и зависимости

‎app/src/main/java/org/opencode/mobile/ui/ChatOverlay.kt‎

Lines changed: 34 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,17 @@ import android.widget.Toast
1414
import androidx.activity.compose.rememberLauncherForActivityResult
1515
import androidx.activity.result.contract.ActivityResultContracts
1616
import androidx.compose.foundation.ExperimentalFoundationApi
17-
import androidx.compose.foundation.background
18-
import androidx.compose.foundation.clickable
19-
import androidx.compose.foundation.combinedClickable
2017
import androidx.compose.foundation.gestures.scrollBy
2118
import androidx.compose.foundation.layout.Column
2219
import androidx.compose.foundation.layout.ExperimentalLayoutApi
23-
import androidx.compose.foundation.layout.Row
2420
import androidx.compose.foundation.layout.fillMaxSize
2521
import androidx.compose.foundation.layout.imePadding
2622
import androidx.compose.foundation.layout.padding
2723
import androidx.compose.foundation.layout.size
2824
import androidx.compose.foundation.lazy.LazyListState
2925
import androidx.compose.foundation.lazy.items
3026
import androidx.compose.foundation.lazy.rememberLazyListState
31-
import androidx.compose.foundation.shape.CircleShape
32-
import androidx.compose.material.icons.Icons
33-
import androidx.compose.material.icons.filled.Add
34-
import androidx.compose.material.icons.filled.Info
35-
import androidx.compose.material.icons.filled.InvertColors
36-
import androidx.compose.material.icons.filled.Mic
37-
import androidx.compose.material.icons.filled.Settings
38-
import androidx.compose.material.icons.filled.TextFormat
3927
import androidx.compose.material3.AlertDialog
40-
import androidx.compose.material3.Icon
4128
import androidx.compose.material3.Surface
4229
import androidx.compose.material3.Text
4330
import androidx.compose.material3.TextButton
@@ -52,15 +39,12 @@ import androidx.compose.runtime.rememberCoroutineScope
5239
import androidx.compose.runtime.saveable.rememberSaveable
5340
import androidx.compose.runtime.setValue
5441
import androidx.compose.runtime.snapshotFlow
55-
import androidx.compose.ui.Alignment
5642
import androidx.compose.ui.ExperimentalComposeUiApi
5743
import androidx.compose.ui.Modifier
58-
import androidx.compose.ui.draw.clip
5944
import androidx.compose.ui.graphics.Color
6045
import androidx.compose.ui.platform.LocalContext
6146
import androidx.compose.ui.platform.LocalFocusManager
6247
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
63-
import androidx.compose.ui.platform.testTag
6448
import androidx.compose.ui.semantics.semantics
6549
import androidx.compose.ui.semantics.testTagsAsResourceId
6650
import androidx.compose.ui.unit.dp
@@ -86,26 +70,31 @@ import org.opencode.mobile.ui.chat.AndroidElevenKeyStore
8670
import org.opencode.mobile.ui.chat.AndroidTtsStore
8771
import org.opencode.mobile.ui.chat.AskActions
8872
import org.opencode.mobile.ui.chat.AudioRecorder
89-
import org.opencode.mobile.ui.chat.CONTEXT_LIMIT
9073
import org.opencode.mobile.ui.chat.ChatAppearance
9174
import org.opencode.mobile.ui.chat.ChatCache
9275
import org.opencode.mobile.ui.chat.ChatFeed
9376
import org.opencode.mobile.ui.chat.ChatFeedState
77+
import org.opencode.mobile.ui.chat.ChatHeader
78+
import org.opencode.mobile.ui.chat.ChatHeaderActions
79+
import org.opencode.mobile.ui.chat.ChatHeaderState
9480
import org.opencode.mobile.ui.chat.ChatInput
9581
import org.opencode.mobile.ui.chat.ChatInputActions
9682
import org.opencode.mobile.ui.chat.ChatInputState
9783
import org.opencode.mobile.ui.chat.ChatPollState
9884
import org.opencode.mobile.ui.chat.ChatUiState
99-
import org.opencode.mobile.ui.chat.ContextGauge
10085
import org.opencode.mobile.ui.chat.ElevenKeySettings
10186
import org.opencode.mobile.ui.chat.FontPanel
10287
import org.opencode.mobile.ui.chat.GaugePanel
103-
import org.opencode.mobile.ui.chat.MCPIndicator
10488
import org.opencode.mobile.ui.chat.McpPanel
10589
import org.opencode.mobile.ui.chat.ModelColorPanel
10690
import org.opencode.mobile.ui.chat.MoltbookCard
10791
import org.opencode.mobile.ui.chat.NarrationInput
108-
import org.opencode.mobile.ui.chat.QuotaBadge
92+
import org.opencode.mobile.ui.chat.PANEL_COLOR
93+
import org.opencode.mobile.ui.chat.PANEL_FONT
94+
import org.opencode.mobile.ui.chat.PANEL_MCP
95+
import org.opencode.mobile.ui.chat.PANEL_MOLTBOOK
96+
import org.opencode.mobile.ui.chat.PANEL_SETTINGS
97+
import org.opencode.mobile.ui.chat.PANEL_STT
10998
import org.opencode.mobile.ui.chat.ServerSendTransport
11099
import org.opencode.mobile.ui.chat.SharedPreferencesAppearanceStore
111100
import org.opencode.mobile.ui.chat.SharedPreferencesVoiceStore
@@ -116,7 +105,6 @@ import org.opencode.mobile.ui.chat.TtsSettings
116105
import org.opencode.mobile.ui.chat.VoiceEngine
117106
import org.opencode.mobile.ui.chat.VoiceHoldActions
118107
import org.opencode.mobile.ui.chat.VoiceSettings
119-
import org.opencode.mobile.ui.chat.ZenMeter
120108
import org.opencode.mobile.ui.chat.abortSession
121109
import org.opencode.mobile.ui.chat.advancePoll
122110
import org.opencode.mobile.ui.chat.answerPendingAsk
@@ -135,16 +123,6 @@ import org.opencode.mobile.ui.chat.vibrate
135123
import java.io.File
136124
import java.util.Locale
137125

138-
// Идентификаторы выпадающих панелей шапки — ключи для [ChatOverlay] переключателя
139-
// toggleHeaderPanel. Строками, а не enum: ключ нужен ровно в двух местах
140-
// (объявление и клик по иконке), а enum здесь стоил бы лишней печати.
141-
private const val PANEL_MCP = "mcp"
142-
private const val PANEL_MOLTBOOK = "moltbook"
143-
private const val PANEL_COLOR = "color"
144-
private const val PANEL_FONT = "font"
145-
private const val PANEL_SETTINGS = "settings"
146-
private const val PANEL_STT = "stt"
147-
148126
// Как часто открытая панель Moltbook перечитывает ledger. Быстрее 10 секунд —
149127
// это долбить базу впустую, медленнее минуты — панель успевает соврать.
150128
private const val MOLTBOOK_PANEL_REFRESH_MS = 10_000L
@@ -157,13 +135,6 @@ private const val MOLTBOOK_PANEL_REFRESH_MS = 10_000L
157135
// ждать до минуты, поэтому задержка сверху не копится.
158136
private const val MOLTBOOK_SCAN_EVERY_MS = 5 * 60_000L
159137

160-
// testTag-метки для проверки геометрии по дампу uiautomator (resource-id).
161-
// Читать их в коде не нужно - это якоря для инструментов, а не логика.
162-
// Сегменты полос сознательно НЕ помечены: 90 ячеек на гейдж дали бы 180 узлов
163-
// ради арифметики, которая восстанавливается из ширины ряда и cellGap.
164-
private const val TAG_CTX_GAUGE = "ctx_gauge"
165-
private const val TAG_ZEN_METER = "zen_meter"
166-
167138
// Миллисекунд в минуте для паузы между обходами сессий.
168139
private const val MS_PER_MINUTE = 60_000L
169140

@@ -1081,153 +1052,31 @@ fun ChatOverlay(
10811052
color = Color(0xFF101010),
10821053
) {
10831054
Column(Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) {
1084-
Row(verticalAlignment = Alignment.CenterVertically) {
1085-
// Статус связи с сервером и пинг: «Online 42ms» зелёным, «Offline»
1086-
// красным. Отдельная от ContextGauge строка — индикатор занятости
1087-
// контекста уехал ниже, чтобы шапка не расползалась: раньше круг
1088-
// занимал всю свободную ширину и отжимал индикаторы вправо.
1089-
QuotaBadge(
1090-
reachable = ui.snapshot?.reachable,
1091-
pingMs = ui.snapshot?.pingMs ?: 0L,
1092-
rejected = ui.snapshot?.notice != null,
1093-
modifier = Modifier.weight(1f),
1094-
)
1095-
// Индикатор MCP-серверов (НЕОНОВЫЙ): «N MCP» + мигающая точка.
1096-
// Зелёный — все N подключённых серверов работают; красный — какой-то
1097-
// из них не работает (или их нет вовсе). Стоит ЛЕВЕЕ выбора цвета.
1098-
MCPIndicator(
1099-
connected = ui.snapshot?.mcpConnected ?: 0,
1100-
total = ui.snapshot?.mcpTotal ?: 0,
1101-
onClick = { toggleHeaderPanel(PANEL_MCP) },
1102-
modifier = Modifier.padding(start = 4.dp),
1103-
)
1104-
// «М» — Moltbook: автономный агент. Буква с бейджем неотвеченных
1105-
// вместо иконки намеренно: это не сервис, а сам агент, и цифра на нём —
1106-
// не «сколько серверов сломано», а «кого мы ещё не ответили».
1107-
MoltbookIndicator(
1108-
awaiting = moltbookStats.awaiting,
1109-
active = showMoltbookPanel,
1110-
onClick = { toggleHeaderPanel(PANEL_MOLTBOOK) },
1111-
)
1112-
// Цветовой пикер для ответов модели. ИКОНКА — готовая «капля»
1113-
// (Material Icons: Icons.Filled.InvertColors) — узнаваемая капля,
1114-
// тонируется ТЕКУЩИМ выбранным цветом ответов (modelColor).
1115-
Icon(
1116-
imageVector = Icons.Filled.InvertColors,
1117-
contentDescription = "Цвет ответов модели",
1118-
tint = modelColor,
1119-
modifier =
1120-
Modifier
1121-
.padding(start = 4.dp)
1122-
.size(22.dp)
1123-
.clip(CircleShape)
1124-
.background(if (showColorPicker) Color(0xFF3A3A3A) else Color.Transparent)
1125-
.clickable { toggleHeaderPanel(PANEL_COLOR) }
1126-
.padding(3.dp),
1127-
)
1128-
Icon(
1129-
imageVector = Icons.Filled.TextFormat,
1130-
contentDescription = "Шрифт ответов модели",
1131-
tint = if (showFontPicker) modelColor else Color(0xFF8A8A8A),
1132-
modifier =
1133-
Modifier
1134-
.padding(start = 4.dp)
1135-
.size(22.dp)
1136-
.clip(CircleShape)
1137-
.background(if (showFontPicker) Color(0xFF3A3A3A) else Color.Transparent)
1138-
.clickable { toggleHeaderPanel(PANEL_FONT) }
1139-
.padding(3.dp),
1140-
)
1141-
Icon(
1142-
imageVector = Icons.Filled.Settings,
1143-
contentDescription = "Настройки гейджа",
1144-
tint = if (showSettings) Color.White else Color(0xFFBDBDBD),
1145-
modifier =
1146-
Modifier
1147-
.padding(start = 4.dp)
1148-
.size(22.dp)
1149-
.clip(CircleShape)
1150-
.background(if (showSettings) Color(0xFF3A3A3A) else Color.Transparent)
1151-
.clickable { toggleHeaderPanel(PANEL_SETTINGS) }
1152-
.padding(3.dp),
1153-
)
1154-
Icon(
1155-
imageVector = Icons.Filled.Mic,
1156-
contentDescription = "Настройки голосового ввода (STT)",
1157-
tint = if (showSttSettings) Color.White else Color(0xFFBDBDBD),
1158-
modifier =
1159-
Modifier
1160-
.padding(start = 4.dp)
1161-
.size(22.dp)
1162-
.clip(CircleShape)
1163-
.background(if (showSttSettings) Color(0xFF3A3A3A) else Color.Transparent)
1164-
.clickable { toggleHeaderPanel(PANEL_STT) }
1165-
.padding(3.dp),
1166-
)
1167-
// Диагностика: состояние сервера, STT-модели и хвост лога serve.
1168-
// Полноэкранный оверлей (DiagnosticsScreen), рисуется поверх всего.
1169-
Icon(
1170-
imageVector = Icons.Filled.Info,
1171-
contentDescription = "Диагностика",
1172-
tint = if (showDiagnostics) Color.White else Color(0xFFBDBDBD),
1173-
modifier =
1174-
Modifier
1175-
.padding(start = 4.dp)
1176-
.size(22.dp)
1177-
.clip(CircleShape)
1178-
.background(if (showDiagnostics) Color(0xFF3A3A3A) else Color.Transparent)
1179-
.clickable {
1180-
// Прячем клавиатуру/фокус: корневой Surface чата сдвинут
1181-
// imePadding(), иначе полноэкранный оверлей «съехал» бы вверх,
1182-
// оставив полосу чата под клавиатурой.
1183-
keyboard?.hide()
1184-
focusManager.clearFocus()
1185-
showDiagnostics = !showDiagnostics
1186-
}.padding(3.dp),
1187-
)
1188-
// «Новая сессия»: тап — создать свежую (старые остаются в списке);
1189-
// long-press — «очистить все сессии» (диалог подтверждения).
1190-
// Подсветка по creatingSession (UI-19), а не showMcpList.
1191-
Icon(
1192-
imageVector = Icons.Filled.Add,
1193-
contentDescription = "Новая сессия (long-press — очистить все сессии)",
1194-
tint = if (creatingSession) Color(0xFF7BD88F) else Color(0xFFBDBDBD),
1195-
modifier =
1196-
Modifier
1197-
.padding(start = 4.dp)
1198-
.size(22.dp)
1199-
.clip(CircleShape)
1200-
.background(Color.Transparent)
1201-
.combinedClickable(
1202-
onClick = { newSession() },
1203-
onLongClick = { confirmClearAll = true },
1204-
).padding(3.dp),
1205-
)
1206-
}
1207-
// Индикатор заполнения контекста — своей строкой ПОД шапкой со статусом.
1208-
// Показывает, сколько накоплено входных токенов сессии относительно
1209-
// лимита модели (CONTEXT_LIMIT). Зелёный → жёлтый → красный по мере
1210-
// приближения к компакту; внутри — процент заполнения.
1211-
Row(verticalAlignment = Alignment.CenterVertically) {
1212-
ContextGauge(
1213-
filled = ui.snapshot?.contextTokens ?: 0L,
1214-
limit = CONTEXT_LIMIT,
1215-
shape = appearance.ctxShape,
1216-
palette = appearance.palette,
1217-
modifier = Modifier.weight(1f).testTag(TAG_CTX_GAUGE),
1218-
)
1219-
}
1220-
// Расход дневной Zen-квоты — ПОД индикатором контекста, отдельной
1221-
// строкой и в его же стиле. В шапку он больше не влезает: там ряд
1222-
// иконок, и любая полоса ломает выравнивание по центру. Зазор
1223-
// настраивается пользователем, чтобы полосы читались как блок, но
1224-
// не слипались.
1225-
ZenMeter(
1226-
used = zenUsedState,
1227-
exact = zenExactState,
1228-
shape = appearance.zenShape,
1229-
palette = appearance.palette,
1230-
modifier = Modifier.padding(top = appearance.barGap.dp).testTag(TAG_ZEN_METER),
1055+
ChatHeader(
1056+
state =
1057+
ChatHeaderState(
1058+
reachable = ui.snapshot?.reachable,
1059+
pingMs = ui.snapshot?.pingMs ?: 0L,
1060+
rejected = ui.snapshot?.notice != null,
1061+
mcpConnected = ui.snapshot?.mcpConnected ?: 0,
1062+
mcpTotal = ui.snapshot?.mcpTotal ?: 0,
1063+
moltbookAwaiting = moltbookStats.awaiting,
1064+
activePanel = activePanel,
1065+
modelColor = modelColor,
1066+
creatingSession = creatingSession,
1067+
diagnostics = showDiagnostics,
1068+
contextTokens = ui.snapshot?.contextTokens ?: 0L,
1069+
zenUsed = zenUsedState,
1070+
zenExact = zenExactState,
1071+
appearance = appearance,
1072+
),
1073+
actions =
1074+
ChatHeaderActions(
1075+
onTogglePanel = ::toggleHeaderPanel,
1076+
onDiagnostics = { showDiagnostics = !showDiagnostics },
1077+
onNewSession = { newSession() },
1078+
onClearAll = { confirmClearAll = true },
1079+
),
12311080
)
12321081
if (showMcpList) {
12331082
McpPanel(ui.snapshot?.mcpServers ?: emptyList())

0 commit comments

Comments
 (0)