@@ -14,30 +14,17 @@ import android.widget.Toast
1414import androidx.activity.compose.rememberLauncherForActivityResult
1515import androidx.activity.result.contract.ActivityResultContracts
1616import androidx.compose.foundation.ExperimentalFoundationApi
17- import androidx.compose.foundation.background
18- import androidx.compose.foundation.clickable
19- import androidx.compose.foundation.combinedClickable
2017import androidx.compose.foundation.gestures.scrollBy
2118import androidx.compose.foundation.layout.Column
2219import androidx.compose.foundation.layout.ExperimentalLayoutApi
23- import androidx.compose.foundation.layout.Row
2420import androidx.compose.foundation.layout.fillMaxSize
2521import androidx.compose.foundation.layout.imePadding
2622import androidx.compose.foundation.layout.padding
2723import androidx.compose.foundation.layout.size
2824import androidx.compose.foundation.lazy.LazyListState
2925import androidx.compose.foundation.lazy.items
3026import 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
3927import androidx.compose.material3.AlertDialog
40- import androidx.compose.material3.Icon
4128import androidx.compose.material3.Surface
4229import androidx.compose.material3.Text
4330import androidx.compose.material3.TextButton
@@ -52,15 +39,12 @@ import androidx.compose.runtime.rememberCoroutineScope
5239import androidx.compose.runtime.saveable.rememberSaveable
5340import androidx.compose.runtime.setValue
5441import androidx.compose.runtime.snapshotFlow
55- import androidx.compose.ui.Alignment
5642import androidx.compose.ui.ExperimentalComposeUiApi
5743import androidx.compose.ui.Modifier
58- import androidx.compose.ui.draw.clip
5944import androidx.compose.ui.graphics.Color
6045import androidx.compose.ui.platform.LocalContext
6146import androidx.compose.ui.platform.LocalFocusManager
6247import androidx.compose.ui.platform.LocalSoftwareKeyboardController
63- import androidx.compose.ui.platform.testTag
6448import androidx.compose.ui.semantics.semantics
6549import androidx.compose.ui.semantics.testTagsAsResourceId
6650import androidx.compose.ui.unit.dp
@@ -86,26 +70,31 @@ import org.opencode.mobile.ui.chat.AndroidElevenKeyStore
8670import org.opencode.mobile.ui.chat.AndroidTtsStore
8771import org.opencode.mobile.ui.chat.AskActions
8872import org.opencode.mobile.ui.chat.AudioRecorder
89- import org.opencode.mobile.ui.chat.CONTEXT_LIMIT
9073import org.opencode.mobile.ui.chat.ChatAppearance
9174import org.opencode.mobile.ui.chat.ChatCache
9275import org.opencode.mobile.ui.chat.ChatFeed
9376import 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
9480import org.opencode.mobile.ui.chat.ChatInput
9581import org.opencode.mobile.ui.chat.ChatInputActions
9682import org.opencode.mobile.ui.chat.ChatInputState
9783import org.opencode.mobile.ui.chat.ChatPollState
9884import org.opencode.mobile.ui.chat.ChatUiState
99- import org.opencode.mobile.ui.chat.ContextGauge
10085import org.opencode.mobile.ui.chat.ElevenKeySettings
10186import org.opencode.mobile.ui.chat.FontPanel
10287import org.opencode.mobile.ui.chat.GaugePanel
103- import org.opencode.mobile.ui.chat.MCPIndicator
10488import org.opencode.mobile.ui.chat.McpPanel
10589import org.opencode.mobile.ui.chat.ModelColorPanel
10690import org.opencode.mobile.ui.chat.MoltbookCard
10791import 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
10998import org.opencode.mobile.ui.chat.ServerSendTransport
11099import org.opencode.mobile.ui.chat.SharedPreferencesAppearanceStore
111100import org.opencode.mobile.ui.chat.SharedPreferencesVoiceStore
@@ -116,7 +105,6 @@ import org.opencode.mobile.ui.chat.TtsSettings
116105import org.opencode.mobile.ui.chat.VoiceEngine
117106import org.opencode.mobile.ui.chat.VoiceHoldActions
118107import org.opencode.mobile.ui.chat.VoiceSettings
119- import org.opencode.mobile.ui.chat.ZenMeter
120108import org.opencode.mobile.ui.chat.abortSession
121109import org.opencode.mobile.ui.chat.advancePoll
122110import org.opencode.mobile.ui.chat.answerPendingAsk
@@ -135,16 +123,6 @@ import org.opencode.mobile.ui.chat.vibrate
135123import java.io.File
136124import 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// это долбить базу впустую, медленнее минуты — панель успевает соврать.
150128private const val MOLTBOOK_PANEL_REFRESH_MS = 10_000L
@@ -157,13 +135,6 @@ private const val MOLTBOOK_PANEL_REFRESH_MS = 10_000L
157135// ждать до минуты, поэтому задержка сверху не копится.
158136private 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// Миллисекунд в минуте для паузы между обходами сессий.
168139private 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