From ce60498a904d9da653bd67984b6fc77283cbcb6a Mon Sep 17 00:00:00 2001 From: claude_code Date: Sun, 21 Jun 2026 16:54:56 +0300 Subject: [PATCH 1/3] docs: track post-0.93.0 share-AI cap change + deferred stream-coverage debt Follow-ups from the multi-aspect review of the e5bc82c7..d4658d4c range. - CHANGELOG: document under [Unreleased] that the default per-workspace hourly public-share assistant cap was lowered 300 -> 100 after the v0.93.0 tag (#62). v0.93.0 shipped 300, so existing deployments that never set SHARE_AI_WORKSPACE_MAX_PER_HOUR drop to 100 on upgrade. - Recreate the still-open Section 3 (AiChatService.stream integration coverage) of the deleted feature-test-coverage-deferred.md as a focused backlog doc so the test debt stays tracked; Sections 1-2 are already closed by the integration harness (PR #115). Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 9 +++++ .../ai-chat-stream-integration-coverage.md | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 docs/backlog/ai-chat-stream-integration-coverage.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b1cb6ebb..43255596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Public share AI: default per-workspace hourly assistant cap lowered + 300 → 100.** The limiter falls back to this default whenever + `SHARE_AI_WORKSPACE_MAX_PER_HOUR` is unset, so a `0.93.0` deployment that + never set the env var has its anonymous public-share assistant hourly cap + cut from 300 to 100 on upgrade. Set `SHARE_AI_WORKSPACE_MAX_PER_HOUR` to + keep the previous limit. (#62) + ## [0.93.0] - 2026-06-21 This release builds on the 0.91.0 AI foundation: admin-defined AI agent roles, diff --git a/docs/backlog/ai-chat-stream-integration-coverage.md b/docs/backlog/ai-chat-stream-integration-coverage.md new file mode 100644 index 00000000..c9c24cbb --- /dev/null +++ b/docs/backlog/ai-chat-stream-integration-coverage.md @@ -0,0 +1,33 @@ +# Отложенные интеграционные тесты `AiChatService.stream` + +Статус: **открыто.** Это остаток от прежнего документа +`feature-test-coverage-deferred.md` (хвост тест-плана PR #49). Два из трёх +его разделов уже закрыты новой интеграционной обвязкой против реального +Postgres/Redis (`apps/server/test/integration/`, PR #115): + +- ✅ **Раздел 1 — repo-тесты против БД.** Закрыт `ai-agent-roles-repo`, + `ai-chat-repo-find-by-creator`, `page-template-references-cascade`, + `workspace-repo-update-setting` (`*.int-spec.ts`). +- ✅ **Раздел 2 — достоверность Lua-окна cost-cap против реального Redis.** + Закрыт `public-share-workspace-limiter.int-spec.ts`. +- ⬜ **Раздел 3 (ниже) — полная интеграция `AiChatService.stream`.** Всё ещё + не реализован; держим запись открытой, чтобы тест-долг не потерялся при + удалении исходного документа. + +## Полная интеграция `AiChatService.stream` (рефактор R1-stream) + +`apps/server/src/core/ai-chat/ai-chat.service.ts`. В PR #49 извлечён и +покрыт только чистый `buildErrorAssistantRecord`. Полные интеграционные +сценарии всё ещё отложены: + +- **Запись чата, упавшего на первом ходу** (`onError`) — ассистентская + запись об ошибке должна сохраняться, даже когда первый ход стрима падает. +- **Жизненный цикл external-MCP клиентов** — клиенты закрываются и при + `throw`, и при `onFinish` (нет утечки соединений). +- **Анти-tamper: история восстанавливается из БД, а не из `body.messages`** — + клиент не может подменить историю через тело запроса. + +Эти сценарии требуют сидирования SDK `streamText` (инъекция/seam колбэков +`onError` / `onFinish` / `onAbort` + `res.hijack`). Отложено, чтобы не +дестабилизировать 287-строчный `stream()`; делать вместе с выносом testable +turn-pipeline. From 569da822b6d8091fd043b646a2d433fa58abc9a2 Mon Sep 17 00:00:00 2001 From: claude_code Date: Sun, 21 Jun 2026 17:11:21 +0300 Subject: [PATCH 2/3] fix(ai-chat): fit full role-card description text in new-chat empty state The colored role cards in the AI chat empty state truncated their admin-configured description with an ellipsis and could clip the top row when the cards overflowed. Make the full text fit: - drop the description lineClamp so the whole text renders - add overflow-wrap: anywhere so long unbreakable tokens (URLs) wrap - switch the cards container to align-content: flex-start so an overflowing top row stays reachable while scrolling (the parent Mantine Center still vertically centers the block when it fits) - widen the card max-width 180px -> 200px for more text room Co-Authored-By: Claude Opus 4.8 --- .../ai-chat/components/role-cards.module.css | 12 ++++++++++-- .../src/features/ai-chat/components/role-cards.tsx | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/client/src/features/ai-chat/components/role-cards.module.css b/apps/client/src/features/ai-chat/components/role-cards.module.css index b47dfd95..94e7c2fb 100644 --- a/apps/client/src/features/ai-chat/components/role-cards.module.css +++ b/apps/client/src/features/ai-chat/components/role-cards.module.css @@ -4,7 +4,11 @@ display: flex; flex-wrap: wrap; justify-content: center; - align-content: center; + /* flex-start keeps the first row reachable when the wrapped cards overflow and + the container scrolls. With align-content: center, an overflowing top row is + pushed out of the scrollable area and becomes unreachable. The parent Mantine + Center still vertically centers the whole block when it fits. */ + align-content: flex-start; gap: 10px; /* Cap the height so a large number of roles scrolls instead of blowing out the empty chat area. */ @@ -21,7 +25,7 @@ justify-content: center; gap: 4px; min-width: 140px; - max-width: 180px; + max-width: 200px; min-height: 90px; padding: 12px 10px; border-radius: var(--mantine-radius-md); @@ -50,4 +54,8 @@ .description { opacity: 0.8; line-height: 1.3; + /* Break long unbreakable tokens (URLs, long foreign words) in the + admin-configured description so they wrap instead of overflowing the card + width now that the line clamp no longer caps the text. */ + overflow-wrap: anywhere; } diff --git a/apps/client/src/features/ai-chat/components/role-cards.tsx b/apps/client/src/features/ai-chat/components/role-cards.tsx index 2551bdbf..75bdd984 100644 --- a/apps/client/src/features/ai-chat/components/role-cards.tsx +++ b/apps/client/src/features/ai-chat/components/role-cards.tsx @@ -45,7 +45,7 @@ function RoleCard({ {name} {description && ( - + {description} )} From a86d0c7c3bc66cb32388413093706a164e4c5b51 Mon Sep 17 00:00:00 2001 From: claude_code Date: Sun, 21 Jun 2026 17:11:21 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(ai-chat):=20always=20show=20generic=20"?= =?UTF-8?q?AI=20is=20typing=E2=80=A6"=20indicator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The typing indicator rendered " is typing…". Show a generic "AI is typing…" instead and keep the role/identity name only in the dimmed interlocutor label above the typing dots. - typing line now always renders t("AI is typing…") - add the "AI is typing…" key to en-US and ru-RU locales - sync stale doc comments that referenced the old text Co-Authored-By: Claude Opus 4.8 --- apps/client/public/locales/en-US/translation.json | 1 + apps/client/public/locales/ru-RU/translation.json | 1 + .../src/features/ai-chat/components/message-list.tsx | 2 +- .../ai-chat/components/show-typing-indicator.test.ts | 2 +- .../src/features/ai-chat/components/typing-indicator.tsx | 8 +++++--- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index fc39a8d9..0f05d69f 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -1147,6 +1147,7 @@ "Take a look at the current document": "Take a look at the current document", "AI agent is typing…": "AI agent is typing…", "{{name}} is typing…": "{{name}} is typing…", + "AI is typing…": "AI is typing…", "Send": "Send", "Stop": "Stop", "Chat menu": "Chat menu", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index 4c40c4bf..816da5d6 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -672,6 +672,7 @@ "Take a look at the current document": "Посмотри текущий документ", "AI agent is typing…": "AI-агент печатает…", "{{name}} is typing…": "{{name}} печатает…", + "AI is typing…": "AI печатает…", "Agent role": "Роль агента", "AI chat": "AI-чат", "AI chat is disabled for this workspace.": "AI-чат отключён для этого рабочего пространства.", diff --git a/apps/client/src/features/ai-chat/components/message-list.tsx b/apps/client/src/features/ai-chat/components/message-list.tsx index b4b4101d..487a7bb7 100644 --- a/apps/client/src/features/ai-chat/components/message-list.tsx +++ b/apps/client/src/features/ai-chat/components/message-list.tsx @@ -43,7 +43,7 @@ interface MessageListProps { const BOTTOM_THRESHOLD = 40; /** - * Whether to show the standalone "AI agent is typing…" indicator. It bridges the + * Whether to show the standalone "AI is typing…" indicator. It bridges the * gap between sending and the first streamed content, so it shows only while a * turn is in flight AND the latest assistant message has nothing visible yet: * - the last message is still the user's (assistant hasn't started a row), or diff --git a/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts b/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts index 5cc023d9..15ab75bc 100644 --- a/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts +++ b/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts @@ -5,7 +5,7 @@ import { showTypingIndicator } from "@/features/ai-chat/components/message-list. /** * Pure-helper tests for the typing-indicator bridging logic that the internal * chat and the public share widget now share. This is the behavior that decides - * whether the animated "AI agent is typing…" placeholder shows in the gap + * whether the animated "AI is typing…" placeholder shows in the gap * between sending and the first streamed token. */ const msg = ( diff --git a/apps/client/src/features/ai-chat/components/typing-indicator.tsx b/apps/client/src/features/ai-chat/components/typing-indicator.tsx index 27373a3e..9dce2a7d 100644 --- a/apps/client/src/features/ai-chat/components/typing-indicator.tsx +++ b/apps/client/src/features/ai-chat/components/typing-indicator.tsx @@ -19,8 +19,10 @@ interface TypingIndicatorProps { * the real assistant message once content starts arriving. * * Mirrors the assistant row layout in MessageItem (the dimmed label), so it reads - * as the assistant's bubble taking shape. The label and typing line use the - * configured identity name when provided, otherwise the generic "AI agent". + * as the assistant's bubble taking shape. The dimmed label uses the configured + * identity name when provided (otherwise the generic "AI agent"), while the + * typing line is always the generic "AI is typing…" (it never includes the + * role/identity name). */ export default function TypingIndicator({ assistantName }: TypingIndicatorProps) { const { t } = useTranslation(); @@ -38,7 +40,7 @@ export default function TypingIndicator({ assistantName }: TypingIndicatorProps) - {name ? t("{{name}} is typing…", { name }) : t("AI agent is typing…")} + {t("AI is typing…")}