Commit Graph

13 Commits

Author SHA1 Message Date
claude code agent 227
044e3f7e6a fix(ai-chat): plural token strings + cover reasoning UI + cleanups (#151 review)
Review of #158 (Request changes) — core logic verified correct; addressed the
test-coverage + localization items:

1. i18n pluralization: the token-count keys were called with {count} but had one
   form, so ru-RU always rendered the genitive ("1 токенов"). Added _one/_other
   (en) and _one/_few/_many (ru: токен/токена/токенов) for both "Thinking… ·
   {{count}} tokens" and "Thinking · {{count}} tokens"; de-duped the PR-added
   duplicate "Thinking" key. Call sites unchanged.
2. ReasoningBlock: new reasoning-block.test.tsx (4 branches: authoritative count
   wins / estimate fallback / header-only when count-but-no-text / body render).
3. Reasoning-token attribution: extracted the #151 anti-double-count rule into a
   pure `reasoningTokensForPart(message)` (single reasoning part -> authoritative
   turn total; multiple/none -> undefined so each estimates). message-item uses
   it; removed the now-dead lastReasoningIndex reduce (review #5). Unit-tested.
6. adopt-chat-id.ts: refreshed 3 stale `chatStreamStartMetadata` ->
   `chatStreamMetadata` comment references.
7. chat-markdown.test.ts: assert the export footer's `reasoning: N` line appears
   when reasoningTokens>0 and is absent at 0/undefined.

Skipped optional #4 (mantine useThrottledCallback): the manual throttle has two
distinct exit paths (turn-end revert-to-null + the captured-total trailing emit)
with no guarding test; remapping risks the streaming behavior — non-blocking.

Client tsc clean; ai-chat suite green (171 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:05:07 +03:00
claude code agent 227
0ebb1adce8 feat(ai-chat): realtime token counter + reasoning tokens, Claude-Code style (#151)
Tokens were only counted post-hoc (onFinish) and the header badge updated only on
chat open/switch; reasoning wasn't requested or shown. Now a counter ticks LIVE
during generation and surfaces reasoning ("thinking") tokens separately, like
Claude Code's `Thinking… · N tokens`.

Architecture (AI SDK v6): no provider gives exact per-token usage mid-stream, so
the live number is a cheap client estimate (chars/≈4) reconciled to AUTHORITATIVE
provider usage at step boundaries and turn end. The useChat per-delta re-render is
the existing realtime engine.

- server: `chatStreamMetadata` now also forwards usage on `finish-step` + `finish`;
  `sendReasoning: true`; persisted `metadata.usage` carries `reasoningTokens`
  (normalized from `outputTokenDetails` or the deprecated field).
- client: pure `count-stream-tokens` (estimateTokens / liveTurnTokens, prefers
  authoritative usage else estimate); `Thinking… · N tokens` in the typing
  indicator; collapsible "Thinking" reasoning block; throttled (~8 Hz) live
  turn-token header badge; `reasoningTokens` in types + Markdown export.

Review fixes folded in:
- v6 `finish-step.usage` is PER-STEP, not cumulative — the server now ACCUMULATES
  a running sum (new pure `accumulateStepUsage`) and sends the cumulative, which
  converges to `finish.totalUsage`, so the live counter never jumps DOWN on a
  multi-step agent turn.
- reasoning double-count: the authoritative turn-total is attributed to a block
  ONLY for a single-reasoning-part (one-step) turn; multi-step blocks each show
  their own estimate (the authoritative total stays in the header).
- no "0" badge flash at turn start (require live > 0, else show context size).
- comment refreshed (finish-step trigger).

Tests: server `accumulateStepUsage` + updated `chatStreamMetadata` (34 in the
suite); client pure-fn tests. Both tsc clean; 162 client ai-chat + the ai-chat
server suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 06:56:14 +03:00
claude_code
554da63b0e fix(ai-chat): keep typing-indicator name gap stable mid-stream
The standalone "Thinking…" indicator's agent-name label switched owners a couple seconds into a turn: at first TypingIndicator rendered name+dots (4px gap), then once useChat materialized the empty/reasoning-only assistant message the name moved to the empty MessageItem row (16px messageRow margin), pushing the dots ~20px away — a visible layout jump.

Introduce a shared pure helper assistantMessageHasVisibleContent() as the single source of truth mirroring MessageItem's render decisions. MessageItem now returns null for an assistant message with no visible content, and typingIndicatorShowsName keeps the name on the indicator until the assistant row has visible content. Exactly one element owns the name throughout the pre-content gap, so the layout no longer reflows.

- new utils/message-content.ts + unit tests (12 cases)
- message-list.tsx: typingIndicatorShowsName uses the helper
- message-item.tsx: early return null when no visible content
- typing-indicator-shows-name.test.ts: updated expectations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 04:36:43 +03:00
claude_code
ebc3b01dc2 feat(ai-chat): mark interrupted turns with a "stopped" notice
A turn that ends without a clean finish now shows a neutral marker, so an
interrupted answer is visible instead of trailing off silently. Errors keep
their existing red banner; this covers the aborted case.

- chat-stopped-notice.tsx: new neutral (gray) notice component
- chat-thread.tsx: live marker driven by useChat onFinish flags — distinguishes
  a manual Stop (isAbort) from a dropped connection (isDisconnect); cleared when
  the next turn streams; flushNext still runs only on a clean finish
- message-item.tsx: per-message marker in reopened history for finishReason
  'aborted' with no error (combined wording, since the server can't tell a
  manual Stop from a dropped connection)
- ai-chat.types.ts: add metadata.finishReason; rowToUiMessage now carries it
- en-US: three new strings

Frontend only — the server already persists partial work and finishReason and
replays it to the model on the next turn (continue, not restart).
2026-06-22 20:56:30 +03:00
claude_code
11d5a75c79 fix(ai-chat): improve error banner layout
The AI chat error Alert stranded the warning icon in the top-left while
the detail text hung indented under the heading, wrapping to 3 narrow
lines with empty space below. Switch to a "full-width detail" layout
(icon + bold heading on the first row, detail spanning full width below)
and extract the markup, previously duplicated in ChatThread and
MessageItem, into a single shared ChatErrorAlert component.

- add apps/client/src/features/ai-chat/components/chat-error-alert.tsx
- use it for the live stream error in chat-thread.tsx (mb="xs")
- use it for the persisted history error in message-item.tsx (mt={4})
- heading/icon use the adaptive --mantine-color-red-light-color so the
  banner stays correct in dark mode

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:54:17 +03:00
claude_code
0bbf94c154 feat(ai-chat): surface the real cause in the error banner
The AI chat error banner always showed a generic "Something went wrong"
with no reason. The server already forwards the provider cause into the
stream (e.g. "Cannot connect to API: read ECONNRESET"), but the client
hid it behind a static heading.

- describeChatError now returns { title, detail }: a short heading naming
  the cause category plus a one-line explanation.
- Add classifyProviderError: maps connection reset, timeout, rate limit,
  context-window overflow, quota and auth failures to clear categories;
  the 403/503 gating responses are preserved; unknown errors fall back to
  the verbatim provider text.
- Match HTTP status codes only as the leading token and textual signatures
  only against the message head (before "| response body:"), so a number
  or phrase in the response-body snippet never mislabels the cause.
- Use the new {title, detail} in all three banners: chat-thread,
  share-ai-widget and the persisted-error banner in message-item.
- Cover the classifier with 20 unit tests (categories + regressions).
2026-06-21 18:54:43 +03:00
claude code agent 227
e8775c45b0 test(ai-chat): cover the conditional assistant-name signature (#108)
Extract the shared assistant-name predicate (resolveAssistantName: trimmed name
or null) used by typing-indicator + message-item, and unit-test the branches
(name shown; whitespace-only -> 'AI agent' fallback; undefined -> fallback).
Behavior-identical (|| -> ?? since the helper returns null).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:52:15 +03:00
claude_code
18105ff6db feat(share-ai): label public chat with the assistant identity name
The anonymous public-share "Ask AI" chat labeled every assistant turn
with the generic "AI agent" even when an Assistant identity (agent role)
was configured. Surface the configured identity name instead, falling
back to "AI agent" when no identity is set.

- server: AiSettingsService.resolvePublicShareAssistantName resolves the
  configured role's name (null when unset/missing/disabled), mirroring
  PublicShareChatService.resolveShareRole; ShareController returns it as
  aiAssistantName on /shares/page-info (only when the assistant is on).
- client: thread aiAssistantName -> ShareAiWidget -> MessageList ->
  MessageItem/TypingIndicator via an optional assistantName prop; the
  internal chat omits it and keeps showing "AI agent".
- i18n: add "{{name}} is typing…" (en-US, ru-RU) for the typing line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:01:07 +03:00
claude code agent 227
e0aac5aa04 feat(share): public-share AI chat reuses the internal chat's presentation (#41)
The public-share widget was a separate minimal impl: plain-text answer, static
'Thinking…', no markdown, no tool-cards. Now it renders through the internal
chat's debugged presentational layer (MessageList/MessageItem/TypingIndicator/
ToolCallCard), so a share gets the same incremental streaming, animated typing
indicator, markdown, and tool-call cards. The share keeps its anonymous
transport (useChat + DefaultChatTransport '/api/shares/ai/stream',
credentials:'omit').

The shared components were already prop-driven (UIMessage[] + isStreaming) with
no transport/auth coupling; made the new props additive optionals (emptyState,
showCitations, neutralizeInternalLinks) all defaulting to current behavior, so
the internal chat is unchanged.

Security (review-caught): rendering assistant markdown on the ANONYMOUS share
made internal links (/p/{id}, /settings/...) clickable, which the old plain-text
render didn't. renderChatMarkdown gains neutralizeInternalLinks (true only on
the share): a one-shot DOMPurify afterSanitizeAttributes hook (added/removed by
reference around a single sanitize) strips href from internal/relative/non-http(s)
links (rendered inert) and keeps external http(s) links with
rel=noopener noreferrer nofollow target=_blank. Tests cover both the link
neutralization and the absence of any global-hook leak into internal renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:04:18 +03:00
claude code agent 227
f1980cf425 test(ai-chat): safety-critical coverage + a11y + pure refactors
Unit tests for the safety-critical paths: crypto secret-box (round-trip,
tamper detection, wrong key), the SSRF guard (blocked ranges + DNS-rebinding),
the ai-chat tools service, the page-embedding repo, and the
assistant-parts/serialization helpers. Those server helpers (assistantParts,
rowToUiMessage, serializeSteps) are exported ONLY for the tests — no runtime
change.

Also: keyboard a11y on the chat history header and conversation rows
(role/tabIndex/Enter+Space), and DRY refactors that move shared logic into one
place (isToolPart -> tool-parts util; buildInitialValues in the MCP form).

The behaviour-changing edits that previously rode along in this commit are
split out into the following two commits, per review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:58:44 +03:00
vvzvlad
51c1830383 fix(ai-chat): keep provider errors visible after a new-chat remount
Surfacing the stream error via useChat().error alone was not enough: on a
brand-new chat the errored turn still fires onFinish -> onTurnFinished, which
adopts the freshly-created chat id and changes the <ChatThread> key, remounting
it with a fresh useChat whose transient `error` is gone. The thread re-seeds
from persisted history, where the assistant row has empty parts and the error
lives only in metadata.error — which was never rendered. Result: an empty
"AI agent" row and no visible error.

- Render the persisted metadata.error inline in MessageItem, so the error
  survives the remount and is also shown in reopened chat history.
- Carry metadata.error onto the rebuilt UIMessage in rowToUiMessage.
- Extract the error formatter into utils/error-message.ts (describeChatError)
  and reuse it for both the live Alert and the persisted error.
- Add metadata.error to the IAiChatMessageRow type.

Client-only; the server already persists metadata.error. No new i18n keys.
2026-06-17 19:41:54 +03:00
vvzvlad
a4b7919753 fix(ai-chat): OpenAI Chat Completions for multi-turn + provider settings, stream UX & errors" -m "Live-stand fixes (OpenRouter / OpenAI-compatible):
- openai provider: use .chat() (Chat Completions) instead of the default callable
  (Responses API), which gateways reject on multi-turn -> 400.
- updateAiProviderSettings: assemble settings.ai.provider via jsonb_build_object
  with ::text-cast bound params + jsonb_typeof self-heal (postgres.js was
  double-encoding it into an array; the ::text cast avoids 'could not determine
  data type of parameter').
- chat agent: drop the hard maxOutputTokens cap (truncated complex tool calls);
  keep a tiny cap only on the test-connection ping.
- testConnection + chat stream: surface the real provider error (statusCode+message)
  to logs and the UI instead of generic masks; never log the API key.
- chat UI: typing indicator, incremental streaming render, tool 'running' status, Stop.

Also bundled (prior uncommitted ai-chat work):
- history 'AI agent' provenance badge; vector RAG (pgvector image + page_embeddings
  + AI_QUEUE indexer + space-scoped semanticSearch); external MCP servers backend
  (@ai-sdk/mcp client, SSRF IP-pinning, encrypted headers, admin CRUD/Test);
  yjs duplicate-instance fix via pnpm patch (single CJS instance server-side).
2026-06-17 04:28:29 +03:00
vvzvlad
44b340dc1a feat(ai-chat): agent write tools, provenance wiring, chat panel + provider settings UI" -m "Backend:
- Add reversible write tools to the per-user agent toolset (page create/update/
  move/soft-delete; comment reply + resolve), exposed under the user's JWT and
  enforced by Docmost CASL; no permanent/force delete (D3).
- Non-spoofable agent provenance: sign actor/aiChatId into the access and collab
  tokens (TokenService), propagate via jwt.strategy onto the request, and set
  pages.last_updated_source/last_updated_ai_chat_id on REST create/update/move and
  comments.created_source/resolved_source/ai_chat_id.
- packages/mcp: add an optional getCollabToken provider (content-edit provenance)
  and guard against empty tokens; service-account /mcp path unchanged.

Frontend:
- Admin 'AI / Models' settings section: provider/model/embedding/base URL, a
  write-only API key field, system prompt, and Test connection.
- AI chat panel (useChat + DefaultChatTransport): conversation list, streamed
  messages, tool-call action log and page citations; header entry point gated on
  settings.ai.chat.

Compile-verified (server nest build + client tsc/vite); not yet live-tested.
Known gaps: history 'AI agent' badge (C3), vector RAG (D), external MCP (E);
chat tool-card citation links pending a fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 02:39:26 +03:00