Чат с агентом МойЧата — из чего собран
Для агента-разработчика: AGENT.md — самодостаточная инструкция: библиотеки, тема, контракт сервера, полные исходники всех модулей и чек-лист приёмки. Проверено: код из файла, разложенный по пустому проекту, собирается без ошибок TypeScript и показывает этот же чат.
Демо на соседней вкладке — копия внешнего вида на чистом HTML/JS. Ниже — модули, библиотеки и ключевые куски настоящей реализации.
1. Библиотеки
| Пакет | Версия | Зачем в чате |
react, react-dom | 19.2 | весь интерфейс; createPortal — всплывашка над выделением |
vite + @vitejs/plugin-react | 8.x | сборка; собирать только NODE_ENV=production |
tailwindcss + @tailwindcss/vite | 4.3 | все стили — классами Tailwind, токены темы в src/index.css |
shadcn + @base-ui/react | 4.20 / 1.6 | кнопки, меню ⚡, диалоги, тултипы, select, checkbox (src/components/ui/*) |
class-variance-authority, clsx, tailwind-merge | — | варианты кнопок и функция cn() |
lucide-react | 1.24 | все иконки: Paperclip, Mic, AtSign, Zap, Brain, ArrowUp, Quote, Play… |
react-markdown + remark-gfm | 10.1 / 4.0 | ответ агента как Markdown: таблицы, списки, код, блок ```details |
sonner | 2.0 | всплывающие уведомления об ошибках |
@fontsource-variable/geist | 5.3 | шрифт Geist, кириллица включена |
react-router-dom | 7.18 | переходы между проектами и ветками |
express, pg | 4.22 / 8.23 | сервер: ручки чата, SSE-поток, PostgreSQL erik_db |
npm i react react-dom react-router-dom react-markdown remark-gfm lucide-react sonner \
@base-ui/react class-variance-authority clsx tailwind-merge @fontsource-variable/geist
npm i -D vite @vitejs/plugin-react tailwindcss @tailwindcss/vite typescript
npx shadcn@latest add button dropdown-menu dialog tooltip select checkbox input popover command
2. Модули фронта
| Файл | Строк | Что делает |
src/components/chat/AgentChatView.tsx | 1249 | весь экран чата: лента, шапка ветки, всплывашка «Цитировать · Делай · Не понял», кнопки поля ввода (короткие ответы, умный режим, фиксация в git, «как дела?», «Стоп»), режим «Поделиться» с отметкой сообщений, живой ход по SSE |
src/components/chat/Composer.tsx | 502 | поле ввода: текст + карточки цитат вперемешку, вложения, диктовка, @-упоминания, Enter/Shift+Enter, слот actions под чужие кнопки |
src/components/chat/Markdown.tsx | 92 | рендер ответа: react-markdown + свои компоненты для таблиц, кода с копированием и раскрывашки ```details |
src/components/chat/PlanCard.tsx | 466 | карточка режима 999: этапы, статусы, журнал |
src/components/chat/Files.tsx | 109 | карточки вложений и превью картинок |
src/components/chat/ThinkingSelect.tsx, ShareMenu.tsx | 40 / 69 | уровень размышления; «Поделиться» — список веток с поиском (Popover + Command/cmdk), отмеченные сообщения уходят цитатами в поле ввода выбранной ветки |
src/lib/drafts.ts | 102 | черновик в localStorage, формат цитаты > [Opus · 12:44], разбор черновика на блоки |
src/lib/upload.ts | 120 | загрузка файлов: до 16 МБ одним телом, больше — кусками по 8 МБ с докачкой (до 3 ГБ) |
src/lib/liveDictation.ts | 317 | диктовка: в Chrome — Web Speech, в остальных — звук на сервер |
src/lib/events.tsx | ~210 | поток событий сервера (fetch('/api/events'), SSE) и хук useServerEvent |
src/lib/api.ts, clock.ts, format.ts | — | запросы с токеном, время сервера для таймеров, форматирование |
3. Сервер
| Файл | Что делает |
server.js | Express на порту 3032, подключает ручки и отдаёт dist/ |
helperServer/agent.js | постоянный процесс агента на каждый чат (claude -p --input-format stream-json), очередь реплик, автосвёртка, фоновые задачи |
helperServer/codexEngine.js и codex*.js | второй движок — GPT через шлюз OpenClaw |
helperServer/events.js | рассылка SSE-событий браузеру |
helperServer/handlers/files.js | загрузка и отдача файлов по подписанной ссылке |
helperServer/plan.js | план режима 999 |
4. Как ходят данные
Composer ──onSend(text, fileIds)──▶ POST /api/agent-chats/:id/messages
│ сервер кладёт реплику в агента
◀── SSE /api/events ─────────────────┘
agent:start → «Работает · шаг…»
agent:tool → строка в блоке «Шаги»
agent:delta → текст ответа печатается
agent:done → сообщение сохранено, кнопки под ним
«Стоп» ──▶ POST /api/agent-chats/:id/stop (мягкий interrupt)
5. Ключевой код
Всплывашка над выделением (AgentChatView.tsx)
const QUOTE_ACTIONS = [
{ label: 'Цитировать', note: undefined, icon: Quote, tone: 'text-primary' },
{ label: 'Делай', note: 'делай', icon: Play, tone: 'text-emerald-400' },
{ label: 'Не понял', note: 'не понял', icon: HelpCircle, tone: 'text-amber-400' },
] as const
document.addEventListener('selectionchange', () => {
const s = window.getSelection()
const text = s && !s.isCollapsed ? s.toString().trim() : ''
// источник — data-quote-src сообщения, где началось и кончилось выделение
setSel({ text, source, x: r.left + r.width / 2, y: r.top })
})
// клик → injectQuote(draftKey, sel.text, sel.source, action.note)
Формат цитаты для агента (drafts.ts)
export function quoteBlock(q: QuoteItem) {
return [...(q.source ? [`[${q.source}]`] : []), ...q.text.split('\n')]
.map((l) => `> ${l}`).join('\n')
}
// агент получает:
// > [Opus · 12:44]
// > Правка на сервере, фронт собирается отдельно…
// делай
Кнопки поля ввода (AgentChatView.tsx → Composer actions)
<Composer onSend={sendFromComposer} mentions={mentionTargets}
actions={<>
<DropdownMenu>…<Zap/> короткие ответы: quick('Отвечай коротко, до 50 слов')…</DropdownMenu>
<Button aria-pressed={smart} onClick={() => setSmart(v => !v)}><Brain/></Button> {/* умный режим */}
<Button onClick={() => quick('Зафиксируй работу в git')}><GithubCat/></Button>
<Button onClick={() => quick('Как дела?')}><MessageCircleQuestion/></Button>
{live && <Button onClick={stop}><Square/> Стоп</Button>}
</>} />
Поделиться в другую ветку (AgentChatView.tsx + ShareMenu.tsx)
// «поделиться» под сообщением → режим отметки, галочки слева
const [picked, setPicked] = useState<Set<string> | null>(null)
const startShare = (id: string) => setPicked(new Set([id]))
// панель над полем ввода: «отмечено N» + кнопка «В ветку…»
<ShareMenu items={shareItems} fromChatId={chatId} onDone={() => setPicked(null)} />
// ShareMenu: Popover со списком веток всех проектов и поиском (cmdk)
const go = (projectId: string, chatId: string) => {
for (const q of items) injectQuote(`draft:agent:${chatId}`, q.text, q.source)
onDone()
navigate(`/p/${projectId}/c/${chatId}`) // ветка открывается, цитаты уже в поле ввода
}
Раскрывашка в ответе (Markdown.tsx)
code: ({ className, children }) => {
if (/language-details\b/.test(className || ''))
return <DetailsBlock text={String(children)} /> // первая строка — заголовок
…
}
6. Что в демо упрощено
- Нет сервера: ответы агента заготовлены и выбираются по тексту реплики.
- Markdown — свой мини-рендер вместо
react-markdown.
- Диктовка печатает заготовленную фразу, файл никуда не отправляется.
- Иконки — тот же Lucide, но UMD-сборка (
lucide.min.js), а не lucide-react.