Запись 60–62. Аудит репозитория в Google AI Studio, модернизация рендеринга и победа над вытеснением буфера истории
⏱️ Время: 2 октября 2026, 08:50 — 12:30 (~3.5 часа)1. Контекст: когда репозиторий отправляется на рентген
Я всегда придерживался простого принципа: архитектор не должен слепо верить в безупречность своего кода только потому, что юнит-тесты горят зеленым, а интерфейс приятно радует глаз. Терминал — коварная среда. В нативных утилитах вроде Alacritty или Kitty отрисовка ложится на плечи кастомных шейдеров видеокарты и ассемблерных оптимизаций. Во Flutter же мы работаем поверх абстракций движка Impeller/Skia, дерева виджетов и компоновщика параграфов.
Чтобы получить независимую, жесткую оценку зрелости проекта, я скормил весь репозиторий Shellit в Google AI Studio, подключив новейшую флагманскую модель Gemini 3.8 Flash.
Задача ставилась без политкорректности: найти любые скрытые архитектурные мины замедленного действия, которые неизбежно взорвутся при реальной эксплуатации серверов с гигабайтными логами и десятками открытых вкладок.
Заключение Gemini 3.8 Flash оказалось предельно точным и местами обескураживающим. Нейросеть буквально разложила по кадрам графический конвейер нашего терминала и указала на три критических архитектурных изъяна, мимо которых мы проходили каждый день.
2. Подводные камни: три диагноза от Gemini 3.8 Flash
Отчет аудита вскрыл следующие фундаментальные проблемы:
-
Паразитный жор батареи на мигании курсора (
BUG-052):
Каждые 550 миллисекунд внутренний таймер каретки вызывал метод_terminal.notifyListeners(). Поскольку терминальный рендерер рисовал фон, текст и курсор на одном общем слое Canvas, этот периодический сигнал приводил к тому, что Flutter в режиме полного простоя (когда на экране не меняется ни единого символа!) заново рассчитывал и перерисовывал все сорок видимых строк терминала. На производительном настольном компьютере это оставалось незаметным, но на ноутбуках и смартфонах это означало постоянную ненужную нагрузку на графический чип и стабильный разряд аккумулятора. -
Угроза зависания UI Isolate при интенсивном выводе (
BUG-053):
Входящий сетевой поток SSH-сокета передавался в терминальную стейт-машину порциями по мере поступления байтов из операционной системы. Если удаленный сервер начинал выплевывать тысячи строк (например, при компиляции ядра или выводеcat access.log), поток пользовательского интерфейса бомбардировался сотнями вызовов в секунду. Отсутствие кадровой синхронизации (VSync) приводило к микрофризам интерфейса, а если порция байтов разрезала многобайтовый символ UTF-8 (например, кириллицу) прямо посередине, терминал выводил уродливые символы-ромбыU+FFFD. -
Микрозазоры и ступеньки в псевдографике (
BUG-054):
Символы рамок Box Drawing (U+2500..U+257F) рисовались как обычные текстовые глифы установленного моноширинного шрифта. Из-за субпиксельного сглаживания и округления координат при различных масштабах экрана (125%, 150%) рамки вhtop,mcилиlazygitраспадались на прерывистые сегменты с неприятными микрощелями.
Диагноз был поставлен, и мы немедленно зафиксировали детальную спецификацию в архитектурном документе TERMINAL_RENDERING_ARCHITECTURE.md.
3. Логика решений с ИИ: три субагента в параллельном строю
Вместо того чтобы вручную переписывать модули в течение многих дней, я применил модульную организацию монорепозитория: сформулировал строгие контракты и запустил в Antigravity трех специализированных субагентов параллельно:
- Субагент сетевого конвейера (
BUG-053): спроектировалTerminalStreamCoalescer. Компонент аккумулирует сетевые чанки байтов и передает их в терминал строго синхронно с частотой обновления экрана черезSchedulerBinding.instance.scheduleFrameCallback. Независимо от того, сколько сотен пакетов прилетело из сокета за 16 миллисекунд, парсинг и перерисовка происходят ровно один раз за кадр. Дополнительно был реализован алгоритм_getIncompleteUtf8TrailingByteCount, который придерживает незавершенные хвосты UTF-8 до следующего кадра, и защитный порог OOM Guard (>512 КБ). - Субагент векторной графики (
BUG-054): создалBoxDrawingVectorRenderer, охвативший весь диапазон из 128 символов псевдографики. Линии вычерчиваются прямыми математическими векторамиcanvas.drawLineстрого по геометрическому центру знакоместа без антиалиасинга. Все рамкиmcиhtopстали монолитными при любом масштабировании экрана. - Субагент слоя каретки (
BUG-052): вынес мигающий курсор в изолированный виджетTerminalCursorOverlayс собственнымRepaintBoundaryи прозрачным для мышиIgnorePointer. Таймер переведен на легковесныйValueNotifier<bool>. В режиме ожидания перерисовка холста терминала полностью прекратилась: нагрузка на процессор упала до честного нуля.
После сборки и интеграции все 198 тестов завершились зеленым успехом. Мы собрали свежий исполняемый файл shellit.exe и перешли к полевым испытаниям.
4. Внезапное открытие на живом тесте: загадка вытесненных строк
Я запустил свежую сборку и дал терминалу экстремальную нагрузку, заставив его непрерывно генерировать гигантские простыни текста.
Результат впечатлял: терминал буквально летал. Никаких намеков на подтормаживание, прокрутка оставалась шелковистой, а процессор в паузах мгновенно засыпал.
Но в процессе скроллинга наверх мой взгляд зацепился за странную аномалию. Когда вывод насчитывает десятки тысяч строк, ранние фрагменты естественно вытесняются из памяти терминала — емкость кольцевого буфера истории не бесконечна. Однако наши фирменные маркеры шелл-интеграции OSC 133 вели себя так, словно ничего не произошло!
Цветные точки на левом поле и засечки на полосе прокрутки продолжали отображаться для команд, чьи строки уже давно растворились в небытии. Хуже того: при попытке кликнуть на такую метку терминал скроллился в самый верх доступной истории и указывал на совершенно случайный текст чужих команд!
Мы устроили архитектурный совет. Первой мыслью было сделать настройку размера буфера, но это борьба со следствием, а не с причиной. Настоящее инженерное решение должно быть честным и логичным:
-
Метки обязаны исчезать по мере вытеснения строк:
Если строка с приглашением ввода (promptLine) была вытеснена из кольцевого буфера, ассоциированный с ней командный блок больше не имеет физического адреса на экране. Оставлять его маркер на левом поле или на скроллбаре — значит вводить пользователя в заблуждение. Метки слева и справа должны скрываться синхронно. -
Связь через живой объект строки:
Вместо хранения статичного целочисленного индекса строки мы связалиShellCommandBlockнапрямую со строкой буфераBufferLine with IndexedItem. Если буфер вытесняет строку, методpromptBufferLine.isEvictedмгновенно возвращаетtrue. -
Информативная индикация:
Пользователь не должен гадать, куда подевались его старые команды. На полосе прокрутки в самом верху появился аккуратный счетчик[▲ N]. При наведении курсора всплывает четкая подсказка: «N ранних команд вытеснено из буфера истории терминала». При этом клавиатурная навигация по командам (Alt+Up/Alt+Down) безопасно игнорирует вытесненные блоки, предотвращая прыжки в пустоту.
5. Итог
Мы еще раз прогнали расширенный набор тестов:
- 202 юнит- и виджет-теста пакета
terminal_uiпройдены со 100% успехом (включая 4 новых теста на динамическое вытеснение строк и индикатор[▲ N]). - Статический анализатор
flutter analyzeпо всему монорепозиторию рапортует об абсолютной чистоте: No issues found! - Сформирована и проверена боевая сборка
shellit.exe. - Shellit обрел эталонный графический конвейер: абсолютно холодный режим ожидания, плавный вывод на любых скоростях и безупречную честность истории команд.
Благодаря жесткому и объективному аудиту Gemini 3.8 Flash в Google AI Studio мы не просто исправили недочеты, а вывели архитектуру терминального рендеринга на качественно новый инженерный уровень.