feat(page-history): интеграция редизайна окна истории — плотный список + мини-календарь heatmap #568
Open
opened 2026-07-12 19:08:27 +03:00 by agent_vscode
·
0 comments
No Branch/Tag Specified
main
develop
feat/557-apikey-phase3a
feat/568-page-history-redesign
feat/561-comments-redesign
feat/530-search-semantic
feat/566-worktime-modal-redesign
feat/370-agent-save-version
recover/506-apikey-ui
feat/370-share-approved
fix/520-recovery-escalation
fix/554-literal-br
feat/529-search-lexical
feat/501-mcp-apikey-auth
feat/506-apikeys-ui
feat/395-work-time
feat/502-mcp-md-modes
feat/492-incremental-render
fix/549-hardbreak-md-canon
feat/542-resolve-undo
fix/505-drawio-strip-comments
fix/503-hardbreak-canon
docs/471-breadcrumb-permission
fix/534-mcp-spaceid
feat/481-version-coherence
fix/535-list-seam-coalesce
fix/525-insert-unloaded
fix/522-internal-links
fix/507-drawio-cyrillic
fix/541-reconnect-timeout
feat/493-converter
feat/370-page-versioning
fix/515-code-emphasis
fix/523-tree-dnd
fix/517-toasts
feat/495-tails
feat/494-mcp-registry-guards
feat/496-suggestions-audit
integ/497-wave1
feat/488-client-fsm
ci/476-publish-gates
feat/489-tool-resilience
feat/487-terminal-lifecycle
fix/486-iter1-prod-fires
refactor/347-client-md-paste
perf/479-getpage-cache
refactor/449-write-invariants
refactor/450-split-client
feat/443-get-page-context
feat/443-get-tree
feat/443-search
docs/415-lossy-descriptions
feat/413-markdown-default
refactor/412-camelcase-tool-names
feat/425-drawio-graph
fix/464-diffdocs-cpu-guard
refactor/411-update-page-markdown
feat/424-drawio-rules
fix/409-invalid-node-validation
refactor/448-generate-inventory
refactor/445-execute-mapping
refactor/446-derive-client-types
fix/447-registry-stamp-ci
fix/444-agent-loop-guards
fix/396-sendnow-detached-run
fix/452-immutable-cache-control
docs/footnote-authoring-comment-cleanup
feat/437-error-diagnostics
fix/442-cursor-pagination
feat/419-footnote-normalize
perf/399-comment-resolve-async
perf/435-collab-token-cache
perf/344-background-rerenders
perf/348-backend-lowhanging
feat/423-drawio-crud
perf/401-collab-hotpath
feat/430-live-reconnect
perf/400-collab-session
refactor/414-dedup-node-ops
feat/417-new-comments-signal
feat/408-createcomment-hints
feat/407-persist-tool-errors
feat/420-footnote-render
dummy-review/mcp-hang-fix
dummy-review/mcp-hang-base
feat/381-resumable-sse-pr1
feat/git-sync-2
docs/agents-workspace-build-order
feature/offline-sync
perf/342-code-splitting
perf/346-compression-cache
feat/196-multi-cursor
perf/343-typing-latency
docs/how-to-test
fix/ai-sdk-partial-output-oom
test/351-generative-converter
feat/371-roles-catalog
refactor/345-server-converter
refactor/294-spec-registry-cont
fix/363-migration-order
fix/e2e-callout-and-gate-build
fix/docker-re2-toolchain
feat/git-sync
fix/media-roundtrip-stability
fix/340-comment-panel-perf
fix/332-deferred-tools
fix/329-ephemeral-suggestions
fix/330-search-in-page
fix/328-resolved-anchor-spam
fix/331-intraline-diff
fix/324-coverage-gate
fix/325-mobile-390
feat/293-A-git-sync-package
feat/300-avatar-oklch
fix/321-banner-mobile
feat/300-avatar-colors
feat/315-comment-suggestions
feat/scroll-restore-stable-wait
feat/300-agent-avatar-stack
feat/300-avatar-polish
refactor/294-tool-spec-registry
feat/scroll-restore-ux
fix/responsive-tablet-sidebar
feature/ai-chat-page-change-observability
image-inline-center
fix/283-short-remap-title
fix/283-slash-layout
image-inline-row
feat/276-ai-chat-dock
fix/269-table-menu-refocus
docs/dev-stand-guide
feat/266-scroll-position
fix/260-collab-docname-slugid
test/244-phase2-tail
fix/262-reindex-progress-realtime
fix/258-changelog-compare-links
fix/244-dataloss-bugs
feat/246-spoiler
feat/221-image-captions
test/244-part-b
feat/251-intentional-clear
fix/embeddings-reindex-progress
refactor/193-tool-spec-registry
fix/255-ws-redis-adapter-leak
fix/252-e2e-open-handles
feat/229-catalog-yaml
feat/243-blob-sandbox
feat/228-inline-footnotes
fix/qa-ui-bugs-216-218
feature/agent-roles-catalog
fix/share-alias-rename
fix/ai-chat-empty-render
feat/191-chat-doc-binding
feat/201-temporary-notes
feat/198-interrupt-agent
feat/ai-chat-full-history
feat/199-ai-generate-title
feat/205-share-aliases
batch/issues-189-187-170
feat/170-mcp-test-button
feat/189-context-badge
feat/198-interrupt-agent-send-now
fix/issues-190-159
fix/ai-chat-new-chat-during-stream
fix/ai-chat-stream-perf
batch/issues-2026-06-25
feat/ai-chat-persistent-history
fix/ai-chat-copy-chat-wysiwyg
fix/ai-stream-reset-resilience
fix/ai-stream-undici-timeout
fix/footnote-review-1227-followup
fix/ai-chat-token-counter-realtime
docs/manual-qa-test-plan
v0.94.1
v0.94.0
v0.93.0
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
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#568
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 "%!s()"
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?
Проблема / Цель
Есть готовый прототип редизайна окна «Page history» —
NewDesign/PageHistoryModal.tsx(Mantine v7, светлая/тёмная, полноразмерная модалка). Идея: слева — панель навигации (мини-календарь-heatmap + плотный список ревизий: одна ревизия = одна строкаглиф · время · автор · [SAVED]), справа — отрендеренная версия страницы с подсветкой изменений; все контролы (Highlight changes, навигация по диффу N/M, Restore) — в одну строку шапки. Агентские ревизии визуально отличаются квадратным глифом роли + «via <кто запустил>».Нужно натянуть визуал на существующую систему истории (
IPageHistory, реальные запросы, restore, права, diff) с минимумом изменений бэка. Единственное реальное изменение бэка — новый дешёвый агрегат «число ревизий по дням» под heatmap (решение принято человеком, см. «Решённые развилки»). Всё остальное — переиспользование существующего поведения. Всё в одной ишью.Для кого: авторы/редакторы страниц, которые разбирают историю правок (в т.ч. агентских — Корректор/Фактчекер из
agent-roles-catalog).Контекст и исследование
Существующая подсистема (переиспользуем, не переписываем механику):
useMediaQuery; мобайл = history-modal-mobile.tsx. Десктоп-тело: history-modal-body.tsx (слеваHistoryList, справаHistoryView+ плавающая панель Highlight changes + N/M).useInfiniteQueryс пагинацией (догрузка страниц поIntersectionObserver,gcTime:0), фильтр «Only versions» черезhistoryKindMeta(kind).version; выбор ставитactiveHistoryIdAtom+activeHistoryPrevIdAtom = resolvePrevSnapshotId(ПОЛНЫЙ_список, id)(не по отфильтрованному/сгруппированному!).historyKindMeta:manual→Saved(blue,version),agent→Agent version(violet,version),boundary/idle/null→Autosave(gray, не version, opacity .55); провенансAgentAvatarStackприlastUpdatedSource==='agent' && agent.IPageHistory:id, title, content?, createdAt(string), lastUpdatedBy, contributors?[], kind?('manual'|'agent'|'idle'|'boundary'|null), lastUpdatedSource?, lastUpdatedAiChatId?, agent?/launcher?(#300). Два ортогональных сигнала:lastUpdatedSource==='agent'= идентичность автора (в т.ч. у агентского автосейва),kind= интенциональность (version vs autosave).HistoryEditor(content, title, previousContent); дифф считается selected↔предыдущий снапшот. Навигация:highlightChangesAtom,diffCountsAtom,useDiffNavigation(scrollViewportRef)(ref — viewport ПРАВОЙ панели; якоряdata-diff-indexэмититHistoryEditor).handleRestoreгрузит снапшот в живые редакторы (mainEditor/titleEditor.setContent) через confirm-модалку («Any changes not versioned will be lost») и гейт CASLspaceAbility.can(Manage, Page) → canRestore. (@Post('/pages/restore')— это восстановление из корзины, не сюда.)@Post('/history'),@Post('/history/info'),@Post('/history/time'). Последний (page-history.service.tscomputeWorkTime) — per-day рабочее время (#395,activeMs/agentMs), черезfindTimelineByPageId(дешёвая проекция безcontent) +bucketByDay(sessions, tz);tz→RangeError→400. Числа ревизий по дням он НЕ отдаёт.Расхождения прототипа ↔ кода (закрываются адаптером/минимальным бэком):
onRestore: api.restore(rev.id). Реально restore — загрузка в редактор + confirm + CASL (переиспользуем).saved: boolean+ один бейджSAVED. Оставляем именно так: бейдж SAVED только дляmanual; агентские версии маркируются глифом роли (не отдельным бейджем — он бы дублировал глиф), автосейвы приглушены без бейджа.calendar.days[].count= число ревизий за день. В системе такого агрегата нет → добавляем (см. дизайн, часть D).dayGroup/atLabelпредформатированы вызывающим. Считаем на клиенте в браузерном tz.Дизайн
Принцип: механику (запросы, дифф, restore, права, пагинация) переиспользуем; меняем компоновку десктоп-окна и плотность строк; добавляем один дешёвый агрегат под heatmap. Мобайл, work-time/TimeWorkedModal — вне scope.
A. Каркас окна (десктоп)
history-modal-desktop.tsx) с одно-строчной шапкой (заголовок, лейбл выбранной версии, тумблер Highlight changes + N/M-навигация по диффу, кнопка Restore, закрытие) и телом «левая навигация 280px | правый рендер версии». В history-modal.tsx десктоп-ветку переключаем на новый body; мобильную ветку (history-modal-mobile.tsx) не трогаем.HistoryView/HistoryEditor, управляемые существующими атомами (activeHistoryIdAtom/activeHistoryPrevIdAtom). Дифф-навигацию — существующимиhighlightChangesAtom/diffCountsAtom/useDiffNavigation;viewportRefправойScrollAreaпрокинуть вuseDiffNavigation(как сейчас в body).resolvePrevSnapshotId(ПОЛНЫЙ плоский список, id). Группировка по дням — чисто презентационная; вresolvePrevSnapshotIdпо-прежнему передаём плоскийhistoryItems, НЕ сгруппированный/отфильтрованный.B. Плотная строка ревизии (адаптер
IPageHistory → строка)atLabel= время;dayISO(YYYY-MM-DDв браузерном tz) — стабильный ключ строки для группировки и jump-to-day;dayGroupLabel= Today/Yesterday/«Mon 12 Jul» (i18n) — только для показа.lastUpdatedSource==='agent' && item.agent→ квадратный глиф роли (agent.name) + «vialauncher?.name»; иначе круглый аватарlastUpdatedBy. (Плотная строка — один аватар; мульти-контрибьюторный стек в плотном списке опускаем осознанно.)SAVEDприkind==='manual'. Агентские версии — без бейджа (глиф уже отличает). Не-version (autosave/boundary/idle/null) — приглушены (opacityкак сейчас), без бейджа.data-day={dayISO}-якорь; леваяScrollAreaполучаетviewportRef— для скролла из календаря (часть E).historyKindMeta(kind).version, manual+agent).C. Restore
useHistoryRestore:confirmRestore(confirm-модалка) + гейтcanRestore(CASL Manage Page) + загрузка снапшота в редакторы. НаивныйonRestoreпрототипа НЕ используем.!canRestoreили выбран новейший снапшот (historyItems[0]— список грузится newest-first, поэтому индекс 0 — глобально новейший даже при пагинации).D. Серверный агрегат «число ревизий по дням» (единственное изменение бэка)
POST /pages/history/day-counts{ pageId, tz }→[{ dayISO, count }]за всю историю за один запрос (без параметраmonth— это убирает повторный full-scan на каждую смену месяца и валидацию месяца).findTimelineByPageId(безcontent) + новый чистый tz-aware per-day count-баркетер, переиспользующий уже протестированное tz-ядро: экспортироватьzonedDayStart/isoDayизwork-time/bucket-by-day.tsи строить счётчик какcreatedAt → isoDay(zonedDayStart(ms, tz), tz) → tally(единый источник DST/дней сbucketByDay, без копипасты).kind ∈ {manual, agent}) — тогда крашеный день в heatmap всегда имеет видимую цель в списке и совпадает с фильтром «Only versions» (см. критерий 6).validateCanView, как/historyи/history/time). Неверныйtz(RangeErrorизIntl) → 400 (как/history/time).E. Мини-календарь + jump-to-day
MiniCalendarиз прототипа + хукusePageHistoryDayCounts(pageId, tz);tz— единый браузерный (Intl.DateTimeFormat().resolvedOptions().timeZone), тот же, что используется для клиентской группировки по дням (часть B) — иначе счётчики календаря разъедутся с днями списка.countиз агрегата (version-ревизии). Навигация месяцев prev/next/today — чисто клиентская (данные за всю историю уже загружены; режем по месяцу локально).dayISO(не по отображаемому лейблу — он относительный и без года), скроллим кdata-day-якорю в левойScrollArea.fetchNextPage()с корректной границей: список newest-first, поэтому тянем страницы, покаdayISOсамой старой загруженной строки не станет<= targetDayISOилиhasNextPage===false. Не «N страниц». Если день так и не достигнут (исчерпали пагинацию) — no-op + короткий тост, без бесконечного цикла.Policy-константы:
heat()), оставляем.manual+agent).Затрагиваемые файлы (примерно):
history-modal-desktop.tsx(порт каркаса + шапки из прототипа); новая плотнаяRevisionRow+ адаптер (dayISO, глиф, бейдж SAVED);MiniCalendar(порт);work-time-стиль сервис/хукusePageHistoryDayCounts; правкаhistory-modal.tsx(десктоп-ветка). ПереиспользуемHistoryView/HistoryEditor,useDiffNavigation,useHistoryReset,resolvePrevSnapshotId,useHistoryRestore,historyKindMeta, атомы.@Post('/pages/history/day-counts')вpage.controller.ts; методcomputeDayCountsвpage-history.service.ts; новый чистый баркетер + экспортzonedDayStart/isoDayизwork-time/bucket-by-day.ts; юнит-тесты баркетера.NewDesign/PageHistoryModal.tsx(исходник-прототип, в корне жить не должен).Границы scope
Не входит: мобильная модалка (
history-modal-mobile.tsx) — без изменений; редизайн work-time/TimeWorkedModal.tsx— отдельно; серверный history-restore-API (restore остаётся клиентским); мульти-контрибьюторный стек в плотной строке (осознанно опущен). Живое обновление heatmap при поступлении новых ревизий во время открытого окна — не требуется (счётчики тянутся при открытии).Критерии приёмки
Проверяемо на стенде (страница с ручными, агентскими и авто-ревизиями):
lastUpdatedBy.SAVEDтолько уmanual; агентские версии — без бейджа (видны глифом); автосейвы приглушены без бейджа.!canRestore.POST /pages/history/day-counts {pageId, tz}отдаёт[{dayISO,count}]за всю историю; неверныйtz→ 400; неавторизованный просмотр → 403; счётчик дня == числу version-строк этого дня в том же tz.dayISOстарейшей строки), а не фиксированным числом; недостижимый день → no-op + тост, без зацикливания.Отказы и наблюдаемость
day-countsупал (400 неверный tz / 5xx / сеть): heatmap показывает пустую сетку (fail-open, без блокировки списка), список и restore работают. Не роняем окно.tz— явный 400 (RangeErrorизIntl), как у/history/time; невалидный день/пустой месяц — пустые ячейки.Совместимость и миграции
Решённые развилки
SAVED(manual); агентские версии маркируются глифом, отдельный «Agent version»-бейдж не вводим (дублировал бы глиф). Кто решил: человек.useHistoryRestore(confirm+CASL),useDiffNavigation, мобильную модалку не трогаем.lastUpdatedSource==='agent', бейдж поkind(дизайн-решение по итогам разбора — иначе агентский автосейв терял бы агентскую идентичность).Связи
/history/time, work-time-ядро и tz-баркетинг — переиспользуемbucket-by-day), #300 (provenance-стек агента).TimeWorkedModal.tsx(отдельный прототип, отдельная ишью).