1.3.0

Added (#388)

  • Изображения-вложения передаются в LLM как image-части (#388): раньше файл, прикреплённый к сообщению чата, доходил до модели только текстовым аппендиксом «## Прикреплённые файлы…» — мультимодальная модель не видела саму картинку (wire-слой providers.Message.Parts и сериализация image-частей в openai-compat/anthropic уже существовали, но загруженные файлы уводились в Attachments и никогда не становились Parts). Теперь AgentLoop.buildUserTurnParts (общий для всех трёх мест персиста user-строки: handleInbound, persistUserMessageOnReceive, coalesce — инвариант byte-identical #344 сохранён) читает image-вложения из workspace агента (гард пути внутри workspace, MIME-набор png/jpeg/gif/webp — пересечение ограничений провайдеров, лимит 5 МиБ по образцу maxEnhancedMediaBytes, ошибки чтения — warn+skip) и добавляет их как ImagePartBase64 в parts текущего хода и в персист messages.media. Гейт видения — AgentLoop.supportsImageInput: приоритет per-invocation model override → default-модель агента (флаг применяется только при совпадении имени с фактической моделью хода, чтобы устаревший DefaultModelID не mislabel-ил ход) → реестр capabilities по имени. В историю LLM-контекста картинки идут только у последних 3 сообщений (trimHistoricImageParts в buildHistoryFromDB) — старые заменяются текстовым плейсхолдером, чтобы base64 не копился в длинном диалоге; БД-строки не мутируются.
  • Настройка модели «Принимает изображения (vision)» (#388): миграция 115 добавляет models.supports_vision (NULL = авто по реестру, 1/0 — явный override для кастомных и локальных моделей с произвольными именами; NULL/auto — консервативный text-only для неизвестных моделей). Store/SQLite/PG CRUD обновлены (в PG общий scan-хелпер pgScanModelInto — ручные Scan-списки 13 колонок заменены, чтобы drift был невозможен), поле провайдится через API без DTO; UI — tri-state селект «Авто (по реестру) / Да — передавать изображения / Нет — только текст» в модалке модели (только для kind=llm) и бейдж «vision» на карточке при явном true. В builtin-реестр capabilities.json добавлена запись glm-5 (modalities text+image, контекст 128k) — longest-prefix с границей -/. покрывает glm-5.3-flash/glm-5-flash, ранее неизвестные реестру и потому text-only.
  • Вложения в чате — файловые чипы вместо текстового блока (#388): миграция 116 добавляет messages.attachments (JSON-массив bus.FileAttachment: file_id/filename/path/mime_type/size); персист user-строк во всех трёх местах пишет дословный текст пользователя + структурные вложения — текстовый аппендикс больше НЕ вшивается в content строки БД, LLM-заметка строится на лету (buildUserContent для live-хода, buildHistoryFromDB для строк истории). Побочный плюс: серверный content теперь байт-в-байт равен отправленному тексту — role+content-фолбек merge #343 для сообщений с вложениями стал совпадать точно (ранее не совпадал вовсе из-за аппендикса). UI: ChatAttachment в types.ts, чипы в user-бабле (FileIcon + имя + размер, стиль чипов композера) — оптимистичный бабл несёт метаданные загрузки сразу, серверная строка заменяет их на merge; клик по чипу с path открывает split-view «Данные» и файл в нём как при выборе в дереве: sidePanel.openDataFile(path) (kind=‘data’ + pending-путь в сторе — панель может быть не смонтирована в момент клика) + revealPath в WorkspaceMDViewer (раскрытие родительских директорий + существующий selectFile, потребление pending после загрузки дерева через $effect). Нормализация JSON-строки attachments — на границе API (endpoints.ts, agentMessages/getFavorites/getMessage). Попутно зафиксирован до этого молчаливо затирающий баг: toggleEnabled на странице моделей слал частичный payload {enabled} на PUT с полной перезаписью — теперь шлёт полную копию карточки (ранее переключение флага затирало name/context_window/фоллбэки).

Docs (#388)

  • docs/features-matrix.md §18: строка «Вложения в чате (#388)» — структурные вложения, чипы с открытием в «Данных», vision-передача с параметрами (флаг модели, реестр, лимит 5 МБ, история — последние 3 сообщения).

Review (#388)

  • ocr review (3 прогона) — закрыты: high «залипающий pendingDataFile» (уход с вкладки «Данные»/закрытие панели гасит запрос на открытие файла — иначе повторное открытие «Данных» кнопкой шапки молча прыгало бы на файл, запрошенный чипом до закрытия); валидация формы ChatAttachment на границе API (структурный guard + конечный неотрицательный size — битый payload не роняет рендер бабла) + console.warn при дропе записей; identity-сохранение normalizeMessageAttachments (уже-null записи не спредятся); === вместо ==; bind:value для vision-селекта + сброс флага при переключении типа на embedding (симметрично фоллбэк-цепочке); неинтерактивный span для чипов без path (не попадают в tab-order); единый formatByteSize в utils/formatSize.ts (было 3 копии: ChatComposer / WorkspaceMDViewer / ChatMessage); санитайз переводов строк в полях attachmentNote (многострочный filename не может вырваться из markdown-буллета); pre-stat проверка размера до чтения файла вложения (os.ReadFile не прерывается по deadline, заявленный Size может врать) + комментарий о 3s-бюджете persistCtx; уточнён комментарий trimHistoricImageParts (бюджет общий для всех ролей — осознанно) и контракт потребления pending-пути в WorkspaceMDViewer. Отклонены с обоснованием: «декодирование messages.media до trim» — позиции image-строк неизвестны до декодирования, decode ~15 МиБ — миллисекунды, главная цель (не слать старые base64 на wire) достигнута; «общий бюджет user+tool картинок» — осознанная токен-гигиена всего окна, задокументировано; «сообщение исчезает молча при раздутом 3s-бюджете» — persist-failure не молчит (receive-path шлёт клиентский error-event, coalesce — Warn, handleInbound ретраит), бюджет ограничен pre-stat кэпом; «file-header в миграциях 115/116» — не соответствует фактическому формату (106/112/114 заголовков не имеют); «обёртка requestMessages» — вкусовщина, конвенция явная. Проверки: go test ./... -count=1, golangci-lint run ./... (0), go vet, npm run check (0), npm test (866), make build-cross — зелёные.