feat(ai-chat): context badge shows current/max (#189) #213
Closed
Ghost
wants to merge 2 commits from
feat/189-context-badge into develop
pull from: feat/189-context-badge
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/244-part-b
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/184-autonomous-agent-runs
vvzvlad:feat/221-image-captions
vvzvlad:feat/git-sync
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/244-dataloss-bugs
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:develop
vvzvlad:feature/offline-sync
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
bug
documentation
duplicate
enhancement
epic
feature
good first issue
help wanted
idea
invalid
needs-human
question
refactor
review/approved
review/changes-requested
review/needs
security
status/blocked
status/done
status/in-progress
status/ready
test
wontfix
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Large multi-phase effort spanning many changes
New functionality request
Good for newcomers
Extra attention is needed
Idea / proposal for discussion
This doesn't seem right
эскалация: нужно решение человека
Further information is requested
Code cleanup / refactoring
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Security / hardening issue
ждёт зависимость blocked_by
закрыто и проверено
в активной работе (мягкая заявка)
специфицировано, не заблокировано, ждёт исполнителя
Test coverage / test infrastructure
This will not be worked on
No Label
feature
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#213
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/189-context-badge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #189
What
The floating AI-chat header badge flipped meaning between states — a live per-turn token counter while streaming vs. the context size at rest — so it "reset to 1" on each prompt and showed two different quantities in the same spot. This makes it consistently show current context / max context, e.g.
572 / 200k.chatContextWindow(tokens) — provider-independent and always exact (per the issue's accepted design;/v1/modelsetc. are unreliable).maxContextTokens) next tocontextTokens, so the client reads both from the last persisted row — no client-side model resolution, survives public shares / future per-role models.current > maxis shown unclamped.Changes
Server:
chatContextWindowadded toAiProviderSettings/PROVIDER_SETTINGS_KEYS/MaskedAiSettings/ResolvedAiConfig; DTO (@IsOptional @IsInt @Min(0));resolve()+getMasked()passthrough; repo parity allowlist;flushAssistantwritesmetadata.maxContextTokenswhen> 0;streamChatpassesresolved.chatContextWindow.Client: new
ContextBadgecomponent (extracted, "current [/ max]", no live mode); removed theliveTurnTokensheader path + deadliveTurnTokens()util (keptestimateTokens);Context window (tokens)NumberInputin AI settings; metadata + service types; i18n (en/ru).Tests
context-badge.test.tsx— current/max display, current-only, no-limit, above-max unclamped, hidden until a size exists, tooltip on hover.flushAssistantwrites/omitsmaxContextTokens; DTO validation forchatContextWindow; parity test covers the new key.Verify
pnpm --filter client exec tsc -b— cleanpnpm --filter server exec tsc --noEmit -p tsconfig.json— cleanpnpm --filter client exec vitest run src/features/ai-chat— 166 passedpnpm --filter server exec jest src/integrations/ai src/core/ai-chat— 399 passed🤖 Generated with Claude Code
Code review — PR #213: context-badge показывает current/max, новая admin-настройка
chatContextWindow(#189)Вердикт: Request changes. Фича по сути не работает: новое числовое поле
chatContextWindowзаписывается в jsonb через::text-каст и превращается в JSON-строку"200000", которую клиентские guard'ыtypeof === "number"отбрасывают — знаменатель/ maxне отрисовывается никогда. Must-fix — починить запись/чтение числового поля и закрыть это DB-round-trip тестом.Объём: дифф
develop…feat/189-context-badge(merge-base3ddc329b), 18 файлов, +305/−347. Прогнаны параллельные аспектные ревьюеры (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture) + judge-проход.Must fix before merge
[regressions] Хранить
chatContextWindowкак JSON-число, а не строку через::text—apps/server/src/database/repos/workspace/workspace.repo.ts:24,267-271Этот PR добавляет
chatContextWindowвAI_PROVIDER_SETTINGS_ALLOWED— это ПЕРВОЕ числовое поле, проходящее черезupdateAiProviderSettings, который строит патч какsql\${v}::text`. DTO валидирует поле как@IsInt()(dto/update-ai-settings.dto.ts:32-36), то есть в SQL уходит реальное JS-число200000, аjsonb_build_object('chatContextWindow', '200000'::text)даёт JSON-строку{"chatContextWindow":"200000"}, а не число. Для всех прежних ключей каст был безвреден, т.к. они уже строки.readProvider/resolve(ai-settings.service.ts:143,159+) делают plain-passthrough без коэрса → возвращается JS-строка"200000". Серверный guardif (extra?.maxContextTokens && extra.maxContextTokens > 0)(ai-chat.service.ts:1263) пропускает строку через JS-коэрс и пишет её вmetadata.maxContextTokensкак строку. Клиент же требуетtypeof max === "number"(ai-chat-window.tsx:307) иtypeof maxContextTokens === "number"(context-badge.tsx:42) — строка не проходит. Итог: админ, настроивший context window, НИКОГДА не видит знаменатель/ max— вся фича PR молча не делает ничего. Fix: вupdateAiProviderSettingsсохранять числовые provider-поля настоящими JSON-числами (ветвить каст поtypeof v—::int/числовой каст илиto_jsonb), либо коэрситьchatContextWindowчерезNumber(...)на чтении вreadProvider/resolve. Обязательно добавить DB-round-trip тест с проверкойjsonb_typeof(settings->'ai'->'provider'->'chatContextWindow') = 'number'`.[documentation] Добавить запись в CHANGELOG
[Unreleased]проchatContextWindowи смену смысла бейджа —CHANGELOG.md(секция[Unreleased])Проект активно ведёт CHANGELOG под
[Unreleased]и документирует прямо сопоставимые изменения: предыдущая admin-настройкаchatApiStyle(#177) имеет и### Added, и### Changedзаписи. Этот PR вводит новую admin-настройкуchatContextWindow(«Context window (tokens)») И user-facing смену поведения — бейдж больше не переключается на live per-turn счётчик во время стрима, а показывает «current / max», — но CHANGELOG не тронут. Не блокирует: ни один документ не противоречит коду, это пробел в полноте документации против собственной конвенции репозитория (AGENTS.md требует CHANGELOG на релизе). Fix: добавить под## [Unreleased]### Added-пункт про настройкуchatContextWindow(драйвер знаменателя/ max) и### Changed-пункт про новый смысл бейджа, со ссылкой(#189).Test coverage
Новый числовой data-flow (
chatContextWindow→maxContextTokens→ бейдж) — это именно то место, где скрылся блокирующий дефект, и он НЕ покрыт сквозным тестом. Добавленныйai-chat.service.spec.tsтестируетcompactToolOutput, а не round-trip контекст-окна;context-badge.test.tsxпроверяет только клиентский рендер при уже-числовомmaxContextTokensи не ловит строковый знаменатель, приходящий с сервера;ai-provider-settings-keys.spec.ts— только parity ключей. Без покрытия:apps/server/src/database/repos/workspace/workspace.repo.ts,apps/server/src/integrations/ai/ai-settings.service.tsНет теста, утверждающего, что после
updateAiProviderSettings(... chatContextWindow: 200000)чтение черезresolve()возвращает число200000(а не строку), и что в jsonb лежитjsonb_typeof = 'number'. Именно этот тест поймал бы must-fix-регрессию. Fix: добавить DB round-trip тест на тип и значение; опционально — серверный тест, чтоmetadata.maxContextTokensоказывается числом.Architecture & design (forward-looking, non-blocking)
chatContextWindowпротив нескольких моделей на workspace —apps/server/src/integrations/ai+ клиентский context-badgePR добавляет один workspace-уровневый скаляр и штампует его как
maxContextTokensна каждый assistant-turn (ai-chat.service.ts:621). Но чат уже гоняет более одной модели на workspace: дефолтныйchatModel, дешёвыйpublicShareChatModelи per-role оверрайды черезroleModelOverride. Один знаменатель корректен максимум для одной из них; комментарии в коде это честно признают, иcontext > maxпоказывается без клампа. Дефект здесь не возникает (degrade graceful), фиксируется как forward-looking constraint.оставить единый workspace-scalar (как сделано).** Effort: S. Pros: ноль доп. работы, одно поле, совпадает с rationale в
ai.types.ts(нет провайдеро-независимого способа узнать окно), unclamped-показ деградирует без крэша. Cons: знаменатель неверен, когда role/public-share идут на модели другого размера (бейдж может читаться210k / 200kдля turn'а на 1M-окне).Code review (re-review) — PR #213: context badge показывает current/max (#189)
Вердикт: Approve. Прошлый блокер закрыт по существу:
chatContextWindowтеперь кастуется как::numericи сохраняется/читается как JSON-число, а не строка"200000". Дельта новых блокеров не вносит.Ре-ревью дельты
d88fe4cd..88199703(3 файлов, +123/−6). Аспекты: stability, conventions, documentation, regressions, test-coverage (параллельные ревьюеры + judge).Статус прошлых блокеров
::text) устранён вworkspace.repo.ts:277-284: cast теперь ветвится по JS-типу —number → ::numeric,boolean → ::boolean, остальное::text. Число попадает в jsonb как JSON-число (jsonb_typeof = 'number',{"chatContextWindow":200000}), а не как"200000".update-ai-settings.dto.ts:34-37—@IsOptional @IsInt @Min(0) chatContextWindow?: number;ai-settings.service.tsиai.types.ts—chatContextWindow?: number; клиентские guard'ыtypeof === "number"(ai-provider-settings.tsx:379) теперь получают реальное число. Никакой строковой коэрции на пути не осталось.Must fix before merge
Нет.
Non-blocking
Нет.
Test coverage
Покрыто. Новая ветка cast'а покрыта integration-тестом
workspace-repo-ai-provider-settings.int-spec.tsна реальном SQL: проверяется и JS-тип возвращённого значения (toBe(200000),typeof === 'number'), и on-diskjsonb_typeof = 'number', и сохранность соседних строковых полей при partial-merge апдейте. DTO-валидацияchatContextWindow(@IsInt @Min(0)) покрытаai-provider-settings-keys.spec.ts.Pull request closed