{"_id":"@aimana/web","name":"@aimana/web","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aimana/web","version":"0.1.0","description":"AIMana web interface","type":"module","engines":{"node":">=22"},"publishConfig":{"access":"public"},"devDependencies":{"@aimana/core":"0.1.0","@codemirror/lang-markdown":"^6.5.2","@codemirror/state":"^6.7.4","@codemirror/view":"^6.43.11","@fontsource-variable/geist":"^5.3.0","@tailwindcss/vite":"^4.3.3","@tanstack/react-query":"^5.102.8","@tanstack/react-virtual":"3.14.10","@testing-library/jest-dom":"^7.0.1","@testing-library/react":"^16.3.3","@testing-library/user-event":"^14.6.7","@types/react":"^19.2.18","@types/react-dom":"^19.2.7","@vitejs/plugin-react":"^6.1.1","class-variance-authority":"^0.7.1","cn":"^0.2.4","codemirror":"^6.0.2","jsdom":"^30.0.1","lucide-react":"^1.41.0","radix-ui":"^1.6.7","react":"^19.2.8","react-dom":"^19.2.8","react-markdown":"^10.1.0","react-router":"^8.3.1","remark-gfm":"^4.0.1","shadcn":"^4.20.1","tailwindcss":"^4.3.3","tw-animate-css":"^1.4.0","vite":"^8.2.2"},"scripts":{"build":"vite build","dev":"vite","preview":"vite preview","typecheck":"tsc --noEmit"},"_nodeVersion":"25.9.0","_id":"@aimana/web@0.1.0","dist":{"integrity":"sha512-gcL+eFwrOEmf4pBXcn+xf2M+50eQoRnD6SYHEr00qqMdwQWqh0eMhuaaDHAU0HpqB9i43r919xKGGGLbdaTIsw==","shasum":"6a60c66f2de7af2d2edf9f938a0aaa19be95b5fd","tarball":"https://registry.npmjs.org/@aimana/web/-/web-0.1.0.tgz","fileCount":11,"unpackedSize":1932024,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFc1URFj/yIvtRE549w/70hI5Ri8b5o4HSu8Jd7mRcUbAiBc89zMeU695rEOvOfAkn3sWTvVshWwjWApVc9Ho4lb2Q=="}]},"_npmUser":{"name":"technofrog","email":"alexei@mikhaltsov.pro"},"directories":{},"maintainers":[{"name":"technofrog","email":"alexei@mikhaltsov.pro"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/web_0.1.0_1788998641938_0.30008375524880226"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-10T00:04:01.713Z","0.1.0":"2026-09-10T00:04:02.194Z","modified":"2026-09-10T00:04:02.393Z"},"maintainers":[{"name":"technofrog","email":"alexei@mikhaltsov.pro"}],"description":"AIMana web interface","readme":"# @aimana/web\n\nВеб-интерфейс AIMana: React 19 SPA, которую в prod раздаёт демон, а в dev Vite с прокси на\nдемон. Открывается командой `aimana open` (см. README `@aimana/cli`).\n\n## Запуск\n\nProd: `pnpm build` собирает `dist/`, демон отдаёт его с SPA-fallback (`GET /` и любой путь вне\nAPI → `index.html`). Dev:\n\n```bash\npnpm --filter @aimana/web dev\n```\n\nи в другом терминале `aimana open --dev`: откроется `http://localhost:5173/?token=…&project=…`.\n`vite.config.ts` проксирует `/projects`, `/runs`, `/claude`, `/health` и `/ws` (WebSocket) на\n`AIMANA_DAEMON_URL` или `http://127.0.0.1:4817`, поэтому браузер работает с одним origin и CORS\nне нужен.\n\n## Сессия\n\n`aimana open` передаёт токен демона и id проекта в query. `initSession(window)` в `main.tsx`\nкладёт их в `sessionStorage` (`aimana.token`, `aimana.project`) и убирает из адресной строки\nчерез `history.replaceState`. Закрытая вкладка = новый `aimana open`; перезагрузка работает.\nБез проекта приложение уходит на `/select-project` и показывает реестр демона.\n\nUI-роуты не должны начинаться с API-префиксов демона (`/projects`, `/runs`, `/claude`,\n`/ws`, `/users`, `/audit`): полная загрузка такого пути попадёт в API и получит 401. Поэтому страница запуска\nживёт на `/run/:id`, выбор проекта на `/select-project`; `routes/router.test.tsx` проверяет\nвсе роуты, хелперы путей в `UI_PATHS`.\n\n## Структура `src/`\n\n```\nmain.tsx                  initSession + createRoot\napp.tsx                   провайдеры: Theme, Tooltip, QueryClient, Api, Ws, Router\nstyles.css                Tailwind 4, токены shadcn (light/dark, OKLCH), .prose для Markdown\nlib/store.ts              createStore/useStore на useSyncExternalStore (сессия, тема, статус WS)\nlib/utils.ts              cn\nsession/session.ts        sessionStore, readSessionFromLocation, initSession, setProject, clearToken\nsession/context.tsx       useSession, useProjectId, useToken\napi/types.ts              контракты демона (зеркало packages/daemon/README.md) + типы core\napi/client.ts             API_PREFIXES, ApiError, createApiClient (fetch + Bearer)\napi/queries.ts            queryKeys (включая specs), taskPath, createQueryClient, ApiContext/useApi,\n                          useProjects, useProjectState, useProjectRuns(id, filters), useRun,\n                          fetchRunEvents, useModels, useTaskSpecs(…, enabled)\napi/templates.ts          useArchitectureTemplates, useArchitecturePreview, useApplyArchitecture,\n                          useStackPresets, useStackPreview, useApplyStackPreset,\n                          usePolicies, usePolicyPreview, useApplyPolicy\ncomponents/architecture-panel  ArchitecturePanel: выбор шаблона, дифф правил, подтверждение перезаписи\ncomponents/stack-panel    StackPanel: выбор пресета стека, что он меняет в PROJECT.md, дифф правил\ncomponents/policies-panel PoliciesPanel: во что разворачивается каждая политика, дифф правил\ncomponents/diff-view      DiffView: построчный дифф, общий для всех трёх панелей\napi/run-stream.ts         useRunStream: история запуска (query) + живые run.event, пропуски, реконнект\nws/client.ts              WsClient: реконнект с backoff, refcount-подписки, ping, on(type | '*')\nws/context.tsx            WsProvider (state.changed → инвалидация Query), useWsStatus, useWsEvent,\n                          useProjectSubscription, useRunSubscription\ntheme/theme.tsx           ThemeProvider, useTheme, ThemeToggle (system | light | dark, localStorage aimana.theme)\ncomponents/ui/*           shadcn/ui (radix, пресет nova): button, card, dialog, tabs, badge, table,\n                          textarea, select, checkbox, input, label, separator, scroll-area, tooltip\ncomponents/status-badge   StatusBadge для статусов фич, тасок, стейджей, гейтов, запусков\ncomponents/progress-bar   ProgressBar (done/total, role=progressbar)\ncomponents/blockers       Blockers: незакрытые зависимости бейджами, feature:/task: без префикса\ncomponents/forms/*        Field, FormError (текст + issues из 422), AddFeatureDialog, AddTaskDialog\ncomponents/forms/model-fields       ModelFields (модель + усилие), modelOptions, withModel, useResolvedModel\ncomponents/forms/generate-plan-dialog  GeneratePlanDialog: модель, усилие, пайплайн, режим ТЗ, замечания\ncomponents/forms/task-settings-dialog  TaskSettingsDialog: PATCH …/settings только изменённым диффом\ncomponents/forms/stage-spec-dialog     StageSpecDialog: ТЗ стейджа, запуск, force, перегенерация, правка\napi/mutations.ts          useCreateFeature, useCreateTask, useStartRun, useCancelRun,\n                          useUpdateFeatureBody/TaskBody/StageBody, useGeneratePlan, useApprovePlan,\n                          useGenerateSpecs, useApproveSpecs, useUpdateTaskSettings, useRunStage\nlib/slug.ts               slugify (транслитерация → kebab id), isValidId (копия regex core)\nlib/run-blocks.ts         groupRunEvents (события → блоки консоли), summarizeToolInput\nlib/format.ts             formatTokens, formatCost, formatDuration, formatTime, formatDate, elapsedMs\nlib/run-title.ts          runTitle (taskRef · стейдж или первая строка промпта)\ncomponents/run/*          blocks (RunBlockView: init, text, thinking, tool, usage, result, error),\n                          event-list (виртуализированный список, follow), run-header (статус, usage, отмена)\ncomponents/run-list       RunList: таблица запусков для истории и compact-список для панели\ncomponents/forms/start-run-dialog  StartRunDialog: промпт, модель из демона, усилие, режим разрешений\ncomponents/markdown       Markdown (react-markdown + remark-gfm, .prose)\ncomponents/markdown-editor  MarkdownEditor (CodeMirror 6), LazyMarkdownEditor для отдельного чанка\ncomponents/doc-editor     DocEditor: один md-документ .aimana/ — просмотр, правка, сохранение\ncomponents/stage-stepper  StageStepper: стейджи таски вертикальным степпером; пропы renderActions\n                          (кнопки в строке) и renderGates (точка монтирования панели гейтов T-047)\ncomponents/plan-panel     PlanPanel: состояние плана, предпросмотр «## План», кнопки по статусу таски\ncomponents/specs-panel    SpecsPanel: пакет ТЗ из GET …/specs, генерация, подтверждение, перегенерация\ncomponents/run-note       RunNote: «Идёт запуск #N · открыть консоль»\ncomponents/gate-list      GateList: гейты стейджа со статусами (только чтение), значение по умолчанию\n                          пропа renderGates у степпера\napi/gates.ts              хуки гейтов: useGateLog (хвост лога), useRunGates, useRetryStage,\n                          useAcceptGate; gateKeys.log с `at` гейта в ключе\ncomponents/gate-panel     GatePanel: гейты стейджа с логом по клику, «Прогнать гейты»,\n                          «Повторить стейдж» и «Принять вручную»\napi/diff.ts               дифф стейджа и таски: diffKeys, useStageDiff, useTaskDiff; оба\n                          включаются флагом «блок открыт», типы — зеркало GitDiffView демона\nlib/unified-diff.ts       parseUnifiedDiff: unified diff → файлы, куски, строки с номерами;\n                          свой разбор, без зависимости\ncomponents/unified-diff-view  UnifiedDiffView: unified diff по многим файлам, файлы\n                          сворачиваются; НЕ diff-view (тот — построчный дифф одного текста)\napi/task-runs.ts          цикл run-all: taskRunKeys, LOOP_STATUS_LABELS/LOOP_REASON_TEXT,\n                          useTaskLoop (404 → null), useProjectTaskLoops, четыре контрола,\n                          writeLoop и useTaskLoopEvents (task-run.changed)\ncomponents/run-all-panel  RunAllPanel: состояние цикла таски, «Запустить всё / Пауза /\n                          Продолжить / Остановить», причина остановки словами, прошлые циклы\ncomponents/forms/run-all-dialog  RunAllDialog: модель, усилие и граница until перед стартом\n                                 и перед продолжением цикла\napi/prompts.ts            очередь промптов: promptKeys, useProjectPrompts (по умолчанию только\n                          ожидающие), useRunPrompts, useAnswerPrompt, usePromptEvents,\n                          suggestedRule\ncomponents/prompt-dialog  PromptDialog: разрешение (команда, аргументы, allow / allow-always /\n                          deny с причиной) и вопрос (варианты подставляются в поле)\ncomponents/prompt-queue   PromptQueue + usePendingPromptCount: ожидающие в правой панели\nlib/use-pending-title.ts  usePendingTitle: (N) AIMana в заголовке вкладки\napi/project.ts            useUpdateProjectBody, useUpdateProjectSettings\ncomponents/project-settings  ProjectSettings: стек, платформы, дефолты, таблица гейтов;\n                          сохраняет диффом\ncomponents/policy-editor  PolicyEditor: чекбоксы политик с описаниями из справочника core\ncomponents/dependencies   Dependencies: «Ждёт» и «Ждут её» ссылками на фичи и таски\ncomponents/policies       Policies: эффективные политики фичи чекбоксами\nlib/deps.ts               parseNodeId, featureDependents, taskDependents (обратные рёбра графа)\nlib/sections.ts           getMarkdownSection: секция `## X` для показа (копия одной функции core)\nlayout/app-shell.tsx      шапка, Sidebar, центр (Outlet с данными проекта), RightPanel; useProjectContext\nlayout/right-panel.tsx    активные запуски проекта, «Запустить Claude», ссылка на историю\nroutes/router.tsx         routes, UI_PATHS, createAppRouter\nroutes/*.tsx              overview (дашборд), project (настройки и правила), feature,\n                          task (стейджи и ТЗ), run (консоль), history (запуски с фильтрами),\n                          projects, errors (ApiErrorView)\ntest/                     setup (jest-dom, cleanup, полифиллы), utils (fakeFetch, renderWithProviders,\n                          renderApp), fake-ws (FakeWebSocket), match-media, fixtures/state\n```\n\nДанные: серверное состояние в TanStack Query (`queryKeys`), локальное в `createStore`.\n`WsProvider` по `state.changed` инвалидирует `['projects', id, 'state']`, по\n`projects.changed` список проектов, по `run.changed` запуски проекта и сам запуск. Страницы\nполучают `{ project, tree, state }` через `useProjectContext()`; сам shell держит подписку\nна проект.\n\n## Дашборд и формы\n\nОбзор (`routes/overview.tsx`) показывает карточку проекта, прогресс фич и тасок, таблицу фич\n(статус, прогресс-бар, блокеры, число тасок, «+ Таска»), «Доступно к старту» из `state.available`\nи «Активные запуски» (`useProjectRuns` с `status=running`, ссылки на консоль). Кнопка «Добавить\nфичу» открывает `AddFeatureDialog`, «Запустить Claude» открывает `StartRunDialog`, на странице\nфичи «Добавить таску» открывает `AddTaskDialog`.\n\nФормы без библиотек: контролируемые поля на UI-kit, `Field` для разметки, `FormError` для\nответа демона (`error` и `issues` из 422). Id подставляется из названия через `lib/slug.ts`\n(кириллица транслитерируется до NFKD, чтобы «ё» и «й» не распадались на базу + диакритику) и\nперестаёт пересчитываться после ручной правки; невалидный id блокирует отправку, формат окончательно\nпроверяет core на демоне. Id таски предлагается как `NNN-slug` по числу тасок фичи. Мутации в\n`api/mutations.ts` на `useMutation`: успех инвалидирует `['projects', id, 'state']` сразу, а\nwatcher демона чуть позже пришлёт `state.changed` (повторная инвалидация безвредна). Так новая\nфича появляется в сайдбаре и таблице без перезагрузки. Диалог на ошибке остаётся открытым,\nзакрытие сбрасывает форму и `mutation.reset()`.\n\n## Фича и таска\n\nСтраница фичи (`routes/feature.tsx`): крошки, статус, прогресс, чипы (приоритет, теги, бюджет,\nдаты), зависимости в обе стороны, таблица тасок со ссылками на страницу таски, описание в\n`DocEditor`, карточка эффективных политик (значения `PROJECT.md` с наложенным override из\n`FEATURE.md`, переопределённые ключи помечены).\n\nСтраница таски (`routes/task.tsx`, `/features/:featureId/tasks/:taskId`): шапка с прогрессом и\nчипами (режим ТЗ, модель, пайплайн, `auto_approve_specs`, PR), зависимости, стейджи вертикальным\nстеппером и `TASK.md` в свёрнутом `DocEditor`. Степпер строится по `TASK.md: stages`, а\n`STAGE-xx.md` подмешивается по id стейджа: файла может не быть, пока не сгенерировано ТЗ\n(тогда стейдж честно пишет «ТЗ не сгенерировано»). Статус из файла важнее статуса в строке\n`TASK.md`. Запуски стейджа — объединение `run_id` строки и `runs` файла, ссылками на консоль.\nЗапуски таски отбираются на клиенте из `GET /projects/:id/runs` по `taskRef`: серверного\nфильтра по таске нет. Гейты берутся из состояния проекта (`doc.frontmatter.gates`), а не отдельным запросом: их\nпишет демон в `STAGE-xx.md`, и `state.changed` обновляет их вместе со всем остальным.\nСодержимое лога — единственное, чего в состоянии нет: его читает `useGateLog` ручкой\n`GET …/gates/:name/log` по клику, и `at` гейта входит в ключ запроса, поэтому после\nперепрогона хвост перечитывается сам. `GatePanel` встаёт в проп `renderGates` степпера;\nбез него степпер рисует read-only `GateList`.\n\n`RunAllPanel` ведёт таску целиком поверх REST цикла из T-033: «Запустить всё» открывает\n`RunAllDialog` (модель, усилие и граница `until` списком стейджей) и шлёт `POST …/run-all`,\nдальше по статусу цикла показываются «Пауза», «Продолжить» и «Остановить». Про паузу и\nостановку честно сказано, что они срабатывают **после текущего стейджа**: демон намеренно не\nубивает идущий запуск, прервать прямо сейчас — это «Отменить» в консоли. Цикл упавший стейдж\nсам не повторяет, поэтому пауза с причиной `gate-failed`, `stage-failed` или `gates-waiting`\nотправляет читателя к кнопкам гейтов стейджа, а не изображает, что «Продолжить» всё починит.\nПричины остановки лежат в `LOOP_REASON_TEXT` — копия `REASON_TEXT` демона под\n`Record<TaskLoopReason, string>`, так что новая причина в демоне ломает `typecheck` здесь.\nЦикл обновляется живьём: `useTaskLoopEvents` слушает `task-run.changed` (подписка на проект\nуже висит в `AppShell`), а `writeLoop` не даёт запоздавшему событию откатить более свежую\nзапись. `GET …/run-all` отвечает 404, когда циклов не было, — это «цикл ещё не запускали», а\nне ошибка.\n\nОбратные рёбра графа («ждут её») core не считает, их даёт `lib/deps.ts` перебором дерева.\n\n### Дифф стейджа и таски\n\nКнопка «Дифф стейджа» стоит в раскрытом стейдже, рядом с его ТЗ; карточка «Дифф таски» —\nвнизу страницы таски, за кнопкой «Показать дифф». Обе ходят в `GET …/stages/:sid/diff` и\n`GET …/tasks/:tid/diff` (`api/diff.ts`) и **только по нажатию**: дифф большой таски весит\nмегабайты, и тянуть их вместе со страницей незачем. По той же причине ключ запроса лежит вне\n`['projects', id, 'state']` — `state.changed` прилетает на каждый чих гейта, а перечитывать\nпатч на каждое событие никто не просил. Свежий дифф — это закрыть и открыть блок.\n\nСырой unified diff разбирает `lib/unified-diff.ts` и показывает `UnifiedDiffView`: файлы\nсворачиваются, добавленное и удалённое подсвечено, номера строк по обе стороны. Разбор\nнаписан здесь, без библиотеки: формат нужен ровно один, а `react-diff-view` тянет за собой\nещё и парсер. Главное правило разбора — не падать: дифф приезжает урезанным по живому, и\nпоследний файл в нём обрывается на середине куска.\n\n**Лимитов два, и оба честные.** Демон режет патч по символам (`limit`, по умолчанию 200 000)\nи говорит об этом полями `truncated` и `size`; веб пишет над диффом, сколько символов из\nскольких показано. Второй лимит — свой: 1500 строк на файл в DOM и свёрнутые по умолчанию\nфайлы, когда их больше шести или строк больше шестисот. Мегабайт минифицированного JS\nпроходит символьный лимит десятком строк, а мегабайт обычного кода кладёт вкладку тридцатью\nтысячами `div`.\n\nКогда показывать нечего, место диффа говорит, чего не хватает, а не изображает пустоту:\nдемон присылает `reason` и русский `note` (не репозиторий, нет коммитов, чисто в дереве,\nпатч не читается), а страница добавляет к ним, что с этим делать — например, «заведи\ngit-репозиторий и включи коммиты в `PROJECT.md: git`». Новые файлы вне индекса перечислены\nотдельным списком: патча у них нет, и делать вид, что стейдж ничего не создал, нельзя. Если\nпатч не приехал вовсе, а счётчики приехали, показывается сводка по файлам.\n\n## Работа с планом и ТЗ\n\nСтраница таски ведёт таску от «есть только название» до запущенного стейджа. `PlanPanel`\nпоказывает состояние плана и ровно те кнопки, что имеют смысл в текущем статусе: в `todo`\n«Сгенерировать план», в `plan-review` «Подтвердить план» и «Перегенерировать с замечаниями»,\nв `planned` «Перепланировать» и «Пересинхронизировать план». Отклонения плана как действия нет:\nэто тот же `POST …/plan` с комментарием. Правка плана — это правка markdown: `approvePlan` на\nдемоне перечитывает секцию `## План` из `TASK.md`, поэтому «Править план» просто раскрывает\n`DocEditor` на `TASK.md`.\n\n`SpecsPanel` появляется, когда план подтверждён, и читает пакет ТЗ из `GET …/specs`, а не из\nдерева: «ТЗ есть» — это вердикт core о секции `## ТЗ`, повторять этот разбор в браузере нельзя.\nОтсюда генерируются и подтверждаются ТЗ (пакетом и по стейджу), отсюда же открываются\nнастройки таски (`TaskSettingsDialog`, `PATCH …/settings` только изменёнными полями).\nКнопка «Запустить» в строке степпера открывает `StageSpecDialog`: ТЗ стейджа, модель, усилие,\nкомментарий и разбор всех отказов демона словами — `previous-not-done` даёт «Запустить всё\nравно» (`force`), `spec-not-approved` даёт «Подтвердить ТЗ», занятый слот — ссылку на активный\nзапуск. Все три действия возвращают 202: результат приезжает не в ответе, а через\n`state.changed`, поэтому панели показывают `RunNote` со ссылкой на консоль и ждут события.\n\n## Настройки проекта\n\nСтраница `/project` (в единственном числе: `/projects` — API-префикс демона, и UI-роут не\nможет с него начинаться) правит `PROJECT.md`: frontmatter формой, тело — тем же `DocEditor`,\nчто и остальные документы.\n\nФорма сохраняет **дифф**: в `PATCH …/settings` уходят только изменившиеся поля. Схема\n`PROJECT.md` — `looseObject`, у проекта бывают ключи, которых форма не показывает, и стирать\nих она не должна. Пустая команда гейта означает удаление гейта, удалённая строка таблицы —\nтоже. Списки языков, платформ и имён гейтов берутся из справочника core и остаются\nподсказками: своё значение можно вписать руками.\n\nПолитика, которой нет в справочнике, показывается с пометкой «дописано руками» — выкинуть\nчужой ключ из формы значит тихо удалить его при следующем сохранении. Чекбокс в этой форме\nменяет только флаг; чтобы политика заодно дописала правило и гейт, её включают в карточке\n«Политики» — под формой так и написано.\n\n### Архитектура\n\nКарточка «Архитектура» на той же странице меняет шаблон архитектуры проекта. Выбор — из\nтого, что реально доступно (`GET …/architecture/templates`: встроенные, пользовательские и\nлежащие в самом репозитории; источник виден прямо в списке). Как только шаблон выбран,\nприезжает предпросмотр (`GET …/architecture/preview?template=`) и показывается **дифф правил\n`PROJECT.md`** построчно, с числом добавленных и убранных строк, плюс строка о том, что\nстанет с `ARCHITECTURE.md`.\n\nДо нажатия «Применить шаблон» не пишется ничего: предпросмотр не трогает ни один файл. Если\n`ARCHITECTURE.md` написан руками, кнопка заблокирована, пока не поставлена отдельная галочка\n«перезаписать», — иначе `POST` вернул бы 409, а человек не понял бы, за что. Шаблон, который\nуже применён, применить нельзя: кнопка выключена и рядом сказано почему.\n\nСвой шаблон кладётся в `~/.aimana/templates/architectures/<id>/` (виден во всех проектах) или\nв `<repo>/.aimana/templates/architectures/<id>/` (только в этом); пресеты и политики лежат\nрядом файлами — `stacks/<id>.yaml`, `policies/<id>.yaml`. Это написано под каждой из трёх\nпанелей: место, где человек ищет свой шаблон, — то же, где он узнаёт, куда его класть.\n\nШаблон, который не прошёл схему, из панели не исчезает: `GET …/architecture/templates` отдаёт\nего в поле `errors`, и под списком видно id, уровень, абсолютный путь файла и поля, на которых\nон развалился. В выпадашке его нет — применять нечего. Так же устроены панели стека и политик.\n\nПравила шаблона живут в `PROJECT.md` между маркерами `aimana:template rules`, поэтому смена\nшаблона меняет только их, а дописанное руками остаётся. Это сказано прямо под панелью: тому,\nкто правит правила в редакторе ниже, важно знать, какую часть файла перепишет следующая\nсмена шаблона.\n\n### Стек\n\nКарточка «Стек» на той же странице разворачивает пресет стека. Список приезжает целиком\n(`GET …/stack/presets`), поэтому у выбранного пресета сразу видно всё: язык, фреймворк,\nменеджер пакетов, платформы, команды гейтов и правила, которые уедут в `PROJECT.md`. Гейтов\nможет не быть вовсе — тогда так и написано: у стека нет команд по умолчанию. Придуманная\nкоманда падала бы на первом запуске, а строка в интерфейсе обещала бы проверку, которой нет.\n\nДальше — предпросмотр (`GET …/stack/preview?stack=`): поля frontmatter списком «было → станет»\nи построчный дифф правил. До нажатия «Применить пресет» не пишется ничего; пресет, который\nуже применён, применить нельзя, и рядом сказано почему.\n\nПравила пресета живут между маркерами `aimana:stack rules` — **отдельно** от\n`aimana:template rules` шаблона архитектуры, поэтому смена стека не трогает правила\nархитектуры. Гейты с теми же именами пресет перезаписывает, остальные оставляет как есть,\nплатформы добавляет к уже указанным; это написано под панелью.\n\n### Политики\n\nКарточка «Политики» на той же странице показывает, **во что разворачивается каждая галочка**,\nдо того как её включили. Список приезжает целиком (`GET …/policies`) вместе с состоянием\nпроекта, поэтому у каждой политики сразу видно: правило, которое уедет в `PROJECT.md`, гейты\nс командами **для стека этого проекта** и дополнительные стейджи с чеклистом. Гейтов может не\nбыть — тогда так и сказано, и сказано почему: проверить это командой нельзя вовсе или для\nстека проекта команды нет. Придуманная команда упала бы на первом гейте.\n\n«Включить» и «Выключить» ничего не пишут: они запрашивают предпросмотр\n(`GET …/policies/preview?policy=&enabled=`) и показывают поля frontmatter «было → станет»\n(включая «убрать» для гейта, который снимается) и построчный дифф правил. Запись происходит по\n«Применить» — `POST …/policies`, а дальше core.\n\nПравила включённых политик живут между маркерами `aimana:policy rules`, отдельно от\n`aimana:template rules` и `aimana:stack rules`. Под панелью сказано и то, чего пока нет:\nдополнительные стейджи доезжают до планировщика текстом правил, отдельной строкой в пайплайне\nони ещё не появляются.\n\n## Приложения\n\nМонорепозиторий редко состоит из одного стека. `/apps` показывает приложения сеткой\nкарточек с **разрешёнными** настройками — с тем, что реально получит запуск внутри\nприложения. Что приложение сказало о себе само, показывает форма на его странице\n(`/apps/:appId`), где это и надо менять.\n\n«Найти приложения» читает манифесты репозитория и **ничего не пишет**: диалог показывает\nуверенность и сигналы словами, а не баллом, чтобы их можно было пойти и проверить на диске.\nId и название правятся до сохранения — id станет ключом в `apps.yaml` и именем каталога.\nПовторный детект не предлагает то, что уже заведено.\n\nНа странице приложения четыре вкладки. В «Стеке» у каждого поля есть «как у проекта», и оно\n**ничего не пишет** в `apps.yaml`: форма отправляет дифф, потому что записать унаследованное\nзначит превратить умолчание в собственное решение приложения — и следующая правка проекта до\nнего уже не дойдёт. Смена языка сбрасывает фреймворк. В «Гейтах» над таблицей сказано, что\nкоманды выполняются в каталоге приложения, а не в корне репозитория; гейты от проекта\nпоказаны отдельно и только на чтение. Шаблоны архитектур отфильтрованы по платформам\nприложения (теги шаблона и есть платформы); приложение, которое про платформы молчит, видит\nвсе. Удаление сначала называет таски, которые на него ссылаются, и только потом удаляет.\n\nНа `/project` настройки подписаны как умолчания и перечисляют приложения, которые их\nпереопределяют. Проект без приложений выглядит как раньше: ни подписи, ни списка, ни\nселектора приложения в форме новой таски — вопрос с одним ответом «весь проект» никто не\nзадавал.\n\n## Человек в контуре\n\nЗапуск, которому нужен неразрешённый инструмент, встаёт и ждёт человека (очередь делает\nдемон, T-022). В браузере это видно из трёх мест сразу: счётчик «N ждут ответа» в шапке,\nкарточка «Ожидают ответа» в правой панели и `(N) AIMana` в заголовке вкладки. Заголовок\nвкладки выбран сознательно: он единственный сигнал, который виден при свёрнутом окне и не\nтребует разрешений браузера.\n\n**Диалог не открывается сам.** Модалка, всплывшая посреди набора текста, крадёт фокус и\nнажатие — а кнопка под этим нажатием выдаёт разрешение на `Bash`. Промпт зовёт, открывает\nчеловек.\n\nРазрешение показывает готовую фразу от SDK, отдельно команду `Bash` (решение принимают по\nней, а не по JSON), причину вопроса, путь вне разрешённых и полные аргументы в свёрнутом\nблоке. Кнопок три: «Разрешить», «Разрешить навсегда» и «Запретить». У «навсегда» под\nкнопкой написано, какое правило появится в `.claude/settings.local.json` — это запись в\nфайл проекта навсегда, и `suggestedRule` считает её тем же способом, что и демон.\n«Запретить» сначала раскрывает поле причины: её текст читает модель.\n\nВопрос показывает варианты кнопками, но отправляется **содержимое поля**: клик по варианту\nлишь подставляет его, а дописать можно что угодно, включая «ни то ни другое, сделай X».\n\nКоды отказа разбираются словами: `prompt-settled` (уже ответили или истёк) блокирует\nкнопки — переспрашивать нечего; `settings-unwritable` кнопки **не** блокирует, потому что\nпромпт остался `pending`: почини файл и ответь снова.\n\nПод очередью — последние отвеченные промпты с именем того, кто ответил: в команде первый\nвопрос про выданное разрешение — чьё оно.\n\n## Команда\n\n`/team` — доступ к демону: кто я, кто ещё есть и что делали. Роль приходит с `/users/me`,\nи веб не гадает по тому, что ему разрешили: пункт меню «Команда» появляется только у\nвладельца, а открытая руками страница объясняет, почему список пустой, вместо пустой\nтаблицы.\n\nТокен нового пользователя показывается **один раз** — демон хранит только хэш. Поэтому\nблок с токеном закрывается кнопкой «Скопировал» и не возвращается.\n\nЖурнал действий — только изменяющие запросы, вместе с отказами: строка `403` в списке\nотвечает на вопрос «кто пытался». Фильтры: пользователь и «только этот проект».\n\n## Правка markdown\n\n`DocEditor` показывает одно тело документа `.aimana/` и умеет его править: «Править» открывает\n`LazyMarkdownEditor`, «Сохранить» шлёт `PATCH` (`useUpdateFeatureBody` / `useUpdateTaskBody` /\n`useUpdateStageBody`), работает `Cmd/Ctrl+S`. Frontmatter из веба не правится вовсе: демон\nменяет только тело (и `updated`), поэтому статусы и стейджи не ломаются. Ошибка сохранения\nпоказывается под редактором и не выкидывает из правки, текст не теряется.\n\nТела документов приходят целиком в `GET /projects/:id/state` (это `ProjectTree` из core),\nотдельного `GET` документа нет. Если файл изменился на диске, пока открыта правка (демон\nдописал результат стейджа, кто-то правит руками), `DocEditor` сравнивает пришедший `source` с\nтем, с которого начали, и показывает предупреждение с кнопкой «Перечитать». Сохранение всё\nравно перезапишет: полноценного `If-Match` в MVP нет намеренно.\n\n## Консоль запуска\n\n`/run/:id` (`routes/run.tsx`) показывает один запуск: шапка (`RunHeader`: статус, kind, taskRef,\nмодель, старт, живая длительность, usage, «Отменить» при `running`, промпт в `<details>`) и\n`EventList`. Данные даёт `useRunStream(runId)` из `api/run-stream.ts`: история\n`GET /runs/:id/events` живёт в TanStack Query под ключом `['run-events', id]` (намеренно не\nпод `['runs', id]`, иначе `run.changed` перечитывал бы всю историю), живые `run.event` и\nдочитанные хвосты копятся в локальном состоянии, `mergeEvents` сливает всё по `seq` без\nдубликатов. Пропуск `seq` (пришло 7 после 5), повторное открытие WS и терминальный статус\nзапуска вызывают `GET /runs/:id/events?after=<lastSeq>`; один запрос в полёте, повторный\nвстаёт в очередь. Запись запуска это `useRun`, `WsProvider` инвалидирует её по `run.changed`.\n\n`groupRunEvents` (`lib/run-blocks.ts`) превращает события в блоки: соседние `assistant.text`\nсливаются в один markdown-блок, `tool.result` прикрепляется к своему `assistant.tool_use`\n(результат без пары показывается блоком `?`), `system.other` пропускается. Блоки рендерит\n`RunBlockView`: tool call свёрнут (имя, краткий вход через `summarizeToolInput`, спиннер,\nгалочка или крест), раскрытие показывает вход JSON и результат; размышления свёрнуты; итог и\nошибки выделены.\n\n`EventList` виртуализирован (`@tanstack/react-virtual`, динамическая высота через\n`measureElement`). Контейнер с нулевой высотой (скрытая вкладка, jsdom) через кастомный\n`observeElementRect` считается за 600 px, иначе virtual-core вернул бы пустой диапазон и в\nтестах ничего бы не рендерилось. Режим follow держит список у низа при новых блоках и\nвыключается, когда пользователь прокрутил вверх больше чем на 48 px; кнопка «Вниз» включает\nего обратно. React-компилятор пропускает этот компонент (warning линта про несовместимую\nбиблиотеку), это ожидаемо.\n\nЗапуск: `StartRunDialog` (промпт, модель, усилие, режим разрешений) шлёт `POST\n/projects/:id/runs` через `useStartRun` и открывает консоль. Список моделей приходит из\n`GET /claude/models` (`useModels`), дефолт берётся из `PROJECT.md defaults.model`, если его\nнет в списке демона, он добавляется как есть; без списка модель вводится текстом. 409\n(активный запуск уже есть) показывает ссылку на него, 503 текст демона. Ctrl/⌘+Enter в\nпромпте отправляет. Правая панель (`layout/right-panel.tsx`) показывает `running` запуски\nпроекта и кнопку запуска; `/history` (`routes/history.tsx`) показывает до 100 последних\nзапусков с серверными фильтрами по статусу и типу (`useProjectRuns(id, filters)`, фильтры в\nключе query после `'runs'`, так что инвалидация по `run.changed` покрывает все варианты) и\nпоиском по промпту на клиенте.\n\n## Компоненты\n\nДобавить компонент shadcn: `pnpm dlx shadcn@latest add -y <name>` в `packages/web`, затем\n`pnpm format`. Импорт через `@/components/ui/<name>`. `Tooltip` требует `TooltipProvider`\n(есть в `app.tsx`; в тестах `renderWithProviders` тоже оборачивает).\n\n## Тесты\n\nVitest в jsdom, проект `@aimana/web` в корневом `vitest.config.ts`:\n\n```bash\npnpm vitest run --project @aimana/web\n```\n\n`renderWithProviders(ui, { session, fetch, ws })` даёт Query + Api (+ Ws); `renderApp({\ninitialEntries, fetch })` рендерит всё приложение на memory-роутере с `FakeWebSocket`.\n`fakeFetch({ 'GET /projects/p1/state': data })` отвечает JSON и считает вызовы.\n`FakeWebSocket.last.open() / message(obj) / serverClose()` управляют сокетом. Radix Select в jsdom\nтребует полифиллы `hasPointerCapture` и `scrollIntoView` (есть в `setup.ts`) и\n`userEvent.setup({ pointerEventsCheck: 0 })` для клика по опциям.\n","readmeFilename":"","_rev":"1-e722ca4b5e3527ed776ffac26432d46d"}