The configured x enabled status dot is implemented and merged via this
branch, so the backlog plan is no longer needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The in-field Clear for the API key fields is implemented and merged via
this branch, so the backlog plan is no longer needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate page-tree row density behind the COMPACT_PAGE_TREE flag
(standard 32px default, compact 26px opt-in). Authored by the local
Claude agent on machine 180.
Make the denser page-tree layout opt-in instead of hardcoded, so row
density can be toggled per deployment via the COMPACT_PAGE_TREE runtime
config flag.
- doc-tree: extract ROW_HEIGHT_STANDARD (32) / ROW_HEIGHT_COMPACT (26);
default the virtualizer row stride to STANDARD density.
- client: isCompactPageTreeEnabled() in lib/config (reads
COMPACT_PAGE_TREE, default true); used by space-tree and shared-tree
to choose the row height.
- server: EnvironmentService.isCompactPageTreeEnabled() and expose
COMPACT_PAGE_TREE through the window runtime config (static.module).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Comments panel was sparse: 12px inner/outer paddings per thread, a
16px gap between avatar and body, body text at the global 16px ProseMirror
size. On a narrow aside column this ate vertical space - few comments per
screen, lots of air.
Tighten strictly inside features/comment (the shared aside frame is left
untouched, so TOC/Details tabs keep their padding):
- Thread Paper: p='sm'->p='xs', mb='sm'->mb='xs' (12->10px).
- Reply-editor Divider: my={4}->my={2}.
- CommentListItem outer Box: pb='xs'->pb={6}; the header Group
(avatar + body) gains gap='xs' (16->10px).
- Font hierarchy: author name sm->xs (14->12px, fw=500 kept), selection
quote sm->xs; comment body via a scoped CSS override on
.commentEditor .ProseMirror: font-size sm (14px) + line-height 1.4,
margin-top 10->4. The page editor is unaffected (the override is
scoped to the comment editor module).
- Selection quote padding 8->6, margin-top 4->2.
- Dropped the unused .wrapper rule (no references).
The PasswordInput for each endpoint API key (Chat / LLM, Embeddings,
Voice / STT) used to show Mantine's built-in visibility toggle (the
'eye') plus a separate 'Clear' link below the field. The eye is useless
here: the key field is a write-only buffer, the stored key never loads
back (the server only returns hasApiKey), so clicking the eye reveals an
empty buffer.
Replace it with a Clear ActionIcon in the field's right section. Passing
a custom rightSection suppresses the built-in eye (Mantine). The Clear
action appears ONLY when a key is stored AND the buffer is empty
(has*ApiKey && form.values.*ApiKey.length === 0); as soon as the user
starts typing a new key, the rightSection falls back to undefined and
the default eye returns - now it is useful (verify what was typed).
After Clear, the handler sets has*ApiKey=false, so the rightSection
flips back too. Self-consistent.
The old Stack wrapper and Anchor 'Clear' link are gone; Anchor is
removed from the @mantine/core import (no remaining usages). The Clear
icon-only button carries type='button' (never submits) and an
aria-label. The two-column 'Model | API key' layout and the write-only
buffer/handler semantics are unchanged.
The header dot on each AI endpoint card (Chat / LLM, Embeddings, Voice /
STT) used to reflect the last 'Test endpoint' probe result - green/red/
gray. That was misleading: a configured-and-enabled endpoint showed GRAY
until someone manually clicked 'Test endpoint'. The dot now reads as the
endpoint's health at a glance, derived synchronously from the live form
values + the workspace feature toggle - never from a network probe.
Four-state model (resolveCardStatus):
ready (green) - configured AND enabled
configured (yellow) - configured but the feature toggle is OFF
off (gray) - not configured (nothing to enable)
warning (orange) - enabled but not configured (a real misconfig:
the feature is on but will not work; surfaced
instead of hidden under gray)
'configured' = model field non-empty AND a base URL available (own OR
inherited from chat for embeddings/STT). The API key is optional - local
servers (Ollama, speaches) work without one. Source of truth is the live
form.values so the dot reacts as the admin types; the persistent feature
toggles drive the enabled axis. The 'Test endpoint' probe result stays
as text under the button - it just no longer paints the dot.
A Tooltip with a human-readable label wraps the dot so the state is not
color-only (colorblind-friendly). resolveCardStatus is exported and
covered by a Vitest spec (4 cases, including the misconfig branch).
## What
Renames the agent-guidance file `CLAUDE.md` → `AGENTS.md` (the opencode-standard name) and prepends a process layer on top of the existing technical/architecture content.
## Changes
- **Rename** `CLAUDE.md` → `AGENTS.md` (git detects 64% similarity, history preserved).
- **New top section "Жизненный цикл задачи"** codifies the workflow we just went through:
1. Sync with `develop`, branch off
2. Implement (per the system-prompt workflow + subagents)
3. **Commit ONLY to Gitea and ONLY as `claude_code`** — never as `vvzvlad`, never push to `origin`/`upstream`
4. Push + PR to `develop`
5. User merges; agent deletes the task's `docs/backlog/<task>.md`
- **New release-cycle section**: before cutting a version, run the three orchestrator skills (test-orchestrator, review-orchestrator, red-team-orchestrator), fix their findings, then tag per the existing procedure.
- **Credentials cheat-sheet**: agent identity, keychain service name (`gitea-claude-code`), Gitea PR API endpoint, base branch, and do-not-push warnings for `origin`/`upstream`.
- **Fix typo**: repo slug is `gitmost`, not `gtimost` (the remote was redirecting on every push). Local `gitea` remote URL is updated to the canonical form.
## Out of scope
No code changes — docs only.
Reviewed-on: #2
Co-authored-by: claude_code <claude_code@vvzvlad.xyz>
Co-committed-by: claude_code <claude_code@vvzvlad.xyz>
Delete the backlog documentation that described the removal of non‑functional DOCX, PDF, and Confluence import features now that the code changes have been merged.
These import paths relied on the private EE module that was deleted from
the repo. In the community build they either threw 'enterprise license'
(DOCX/PDF) or silently no-op'd (Confluence). The frontend buttons were
already removed in 38064064; this cleans up the dead backend stubs.
- import.service.ts: drop processDocx/processPdf methods, their dispatcher
branches, the pageId computation + insertPage spread, and the now-unused
moduleRef param/ModuleRef import
- file-import-task.service.ts: drop the Confluence branch and the now-unused
moduleRef param/ModuleRef import
- import.controller.ts: restrict file extensions to .md/.html and zip
sources to generic/notion; update the error message accordingly
- file.utils.ts: remove Confluence from the FileImportSource enum
- features.ts: remove the unused CONFLUENCE_IMPORT/DOCX_IMPORT/PDF_IMPORT
feature keys
The isConfluenceImport logic in import-attachment.service.ts is intentionally
left in place (real shared attachment-parsing code, not a stub); its removal
is a separate, riskier refactor.
The two catch blocks in importPage() threw an opaque "Error processing file
content" / "Failed to create imported page" BadRequest, hiding the real cause
from the HTTP response. This made a production 400 regression impossible to
diagnose without server log access, and violated the project convention that
errors must never be swallowed.
Extract `${err.name}: ${err.message}` into both the log (full err object kept
for the stack) and the thrown BadRequestException. Inner processMarkdown/
processHTML rethrowing catches and the EE processDocx/processPdf license
catches are left unchanged.
Local reproduction of the happy-dom 14->20 theory failed (full import chain
+ 22 edge cases pass on happy-dom@20.8.9), so the root cause is still pending
the now-visible reason from a recurring 400. Diagnostic script test-import.tsx
added; backlog doc updated with findings.
Rename the macOS app repository reference in README.md and README.ru.md
from vvzvlad/docmost-app to vvzvlad/gitmost-app (both the URL and the
link label), reflecting the renamed repo.
- Remove automatic panel opening in handleAddComment
- Remove automatic panel opening in handleAddReadOnlyComment
- Keep panel open on click for existing comments in editor
Reduce DocTree row stride from 32px to 26px for a denser sidebar tree,
and fix the selection/hover highlight that looked unbalanced at the
tighter spacing.
Root causes:
- The virtualized <li> had no explicit height, so `.node`'s height:100%
collapsed to content height; combined with the asymmetric
`[role="treeitem"] { padding-bottom: 2px }` rule, row content was
pushed to the top of the highlight pill (icon glued to the top edge).
- NodeMenu / CreateNode action icons used the default Mantine ActionIcon
size (md = 28px), overflowing the tighter 26px row stride onto
neighbouring rows.
Changes:
- doc-tree.tsx: rowHeight 32 -> 26; give each row <li> a definite
height = rowHeight.
- tree.module.css: rowWrapper fills the slot (height:100%); node pill is
inset and vertically centered (height: calc(100% - 4px)); drop the
asymmetric [role="treeitem"] padding-bottom.
- space-tree-node-menu.tsx / space-tree-row.tsx: action icons size={20}.
- share.module.css: drop now-dead .treeNode padding-bottom override.
Verified in an isolated browser harness: highlight content is centered
(2.8/2.8px) and nothing overflows the row stride.
Add a dedicated section describing the licensing conflict between the AGPL‑3.0‑licensed web client and App Store DRM/usage rules. Explain why this is a non‑technical blocker, outline possible distribution approaches (server‑loaded client, OTA updates, PWA, sideload), and recommend confirming the chosen path before implementing any iOS wrapper code.
Reduce the recent/favorites/created-by-me list tables from
verticalSpacing="sm" (12px Td padding) to a numeric 6px, removing
~12px of extra height per row so the home page lists pack closer
together. The shared RecentChanges table also drives the space home
view, so both stay consistent.
Drop the Overview home link that sat between the space switcher and
the Pages section in the authenticated space sidebar. Remove the JSX
block and clean up the now-unused imports (UnstyledButton, IconHome,
useLocation, getSpaceUrl) and the local `location` variable.
Add markdown files describing the per‑user authentication mechanism and the ability to expand or collapse all nodes in the page tree, improving guidance for developers working with the MCP backlog feature.
Reduce DocTree default rowHeight from 32px to 26px so sidebar page
tree rows pack closer together. The virtualizer uses rowHeight as the
row stride (estimateSize + translateY), and row content is only ~22px
tall, so the previous 32px left an ~8px gap between nodes. Both the
space tree and the shared (public) tree inherit the default, so both
become denser and stay consistent.
Rework the space sidebar:
- remove the "New page" and "Search" menu items (search stays in the app
header; page creation stays via the "+" button in the Pages section)
- move "Space settings" into a gear icon next to the current space name
- drop the searchable space popover and render all spaces as an
always-visible grid of fixed-width cards (icon + name), several per row,
sorted alphabetically, with the active space highlighted
- always inject the active space into the grid so it stays highlightable
even when the user has more than the 100-space API page limit
The shared SpaceSelect component is left untouched (still used by the
move/copy page modals).
Add two new backlog documentation files:
- ai-chat-collapse-on-page-focus.md describing auto‑collapse behavior for the AI chat window.
- comments-panel-density.md outlining UI density improvements for the comments panel.
Add a backlog design note for making page-tree realtime updates
server-authoritative instead of client-relayed.
Problem: page content syncs via Yjs/Hocuspocus (server-authoritative),
but tree create/move/delete is broadcast by the originating browser only,
so non-UI creation paths (AI agent, MCP, REST API, import) and lost-event
races leave other clients' sidebars stale.
The note specifies a WsService.emitTreeEvent broadcaster, WsTreeService
broadcast helpers, a PageWsListener on PAGE_CREATED/SOFT_DELETED/DELETED/
MOVED/RESTORED, event-payload enrichment to avoid the in-transaction
re-fetch race, a dedicated PAGE_MOVED event, removal of the client relay,
plus edge cases, work breakdown, tests, alternatives and open questions.
list_pages gains an opt-in `tree` parameter on both surfaces (the
@docmost/mcp server tool and the AI-chat agent tool), which share the
same DocmostClient.listPages. Default behavior (recent-by-updatedAt flat
list) is unchanged.
- client.ts: listPages(spaceId?, limit=50, tree=false); when tree is
true it requires spaceId (throws a specific error otherwise), walks the
sidebar tree via the existing bounded/cycle-safe enumerateSpacePages,
and returns a nested tree; limit is ignored in tree mode.
- lib/tree.ts: new pure buildPageTree() — lean nodes { id, slugId, title,
children? }, children sorted by position (code-unit order), orphans
promoted to roots, cycle-safe.
- index.ts + ai-chat-tools.service.ts: expose `tree` in the tool schemas
and descriptions; docmost-client.loader.ts: mirror the new signature.
- tests: add packages/mcp/test/unit/tree.test.mjs (nesting, ordering,
lean shape, orphan promotion, cycle/self-reference safety).
- rebuild @docmost/mcp (build/ is tracked and loaded at runtime).
Wrap the logo link and the APP_VERSION text into a single bottom-aligned
Group so they read as one lockup ("gitmost v0.9..."). Move the version
styling into a new .brandVersion CSS class: shrink it from 12px to 10px,
keep the dimmed color and selectability, and lift it via margin-bottom so
its text baseline sits on the wordmark baseline of the 30px desktop logo
(derived from the logo SVG geometry). Drop the redundant lh prop.
Remove outdated process sections from several backlog markdown files and add new backlog items for AI chat step limits, endpoint status config, and API key field UI improvements.
The transcript force-scrolled to the bottom on every streamed delta because
the auto-scroll effect ran unconditionally whenever the messages array identity
changed. Scrolling up to read earlier messages was impossible — each token
yanked the view back down.
Implement a "stick to bottom" pattern in MessageList:
- track whether the viewport is pinned to the bottom via a scroll listener
(pinnedToBottomRef, BOTTOM_THRESHOLD = 40px);
- only auto-scroll while pinned; a freshly sent user message always re-pins;
- attach the scroll listener via a [hasScrollArea] dependency so a brand-new
empty chat (whose ScrollArea mounts only after the first message) wires it up;
- guard the effect's own scrollTop write (programmaticScrollRef) so it is not
misread as a user scroll.
The floating AI-chat header badge summed metadata.usage (AI SDK
totalUsage, all steps) across every assistant row, showing the
cumulative tokens SPENT — which grows each turn as history is re-sent.
Replace it with the conversation's CURRENT context size.
- server: persist metadata.contextTokens in streamText onFinish from the
final-step `usage` (inputTokens + outputTokens ≈ current context
window occupancy); keep usage: totalUsage for back-compat/fallback
- client: derive the badge from the most recent assistant row's
contextTokens (fallback to that row's usage total for older chats)
instead of summing all rows
- types: add metadata.contextTokens to IAiChatMessageRow
- i18n: rename badge label "Tokens used in this chat" -> "Current
context size" (en-US)
No DB migration needed (metadata is a JSON column).
Add docs/backlog/stt-providers-and-async.md: how to add new synchronous STT
request formats (Deepgram, native Gemini, ElevenLabs) via the explicit
sttApiStyle axis, which providers are inherently async and don't fit the
current sync model, and a target job-based async architecture (BullMQ job
table, sync+async unification, polling -> push -> live streaming) with the
migration path and security/cleanup considerations.
Add docs/streaming-dictation-plan.md — a design document for true
"text appears as you speak" dictation via the OpenAI Realtime API.
- Maps the current batch dictation flow (client MediaRecorder -> single
blob -> POST /ai-chat/transcribe) and why streaming is impossible there.
- Documents the Realtime API contract (transcription session, ephemeral
token, pcm16 audio, input_audio_buffer.append, input_audio_transcription
delta/completed events, server_vad).
- Recommends a server-side WS proxy transport (key stays server-side,
SSRF-guarded, provider-agnostic via sttBaseUrl) over direct browser
WebRTC, and a ProseMirror decoration for interim text with final-only
commit to avoid polluting Yjs collab/history.
- Covers config additions, AudioWorklet PCM16 capture, security per repo
conventions, edge cases, phased rollout, risks, and impacted files.
Replace the implicit `hostname endsWith openrouter.ai` detection with an
explicit, admin-chosen provider field `sttApiStyle` ('multipart' = OpenAI-
compatible multipart /audio/transcriptions; 'json' = OpenRouter-style JSON +
base64 input_audio). The transcription path now branches on the stored field,
not on the URL — nothing hidden from the admin.
- ai.types: add SttApiStyle + STT_API_STYLES; field on AiProviderSettings and
MaskedAiSettings (resolved via ResolvedAiConfig).
- update-ai-settings.dto: validate sttApiStyle with @IsIn(STT_API_STYLES).
- ai-settings.service: plumb sttApiStyle through resolve()/getMasked() and the
non-secret update whitelist; workspace.repo: add it to the ALLOWED array so it
persists.
- ai.service: drop isOpenRouter(); transcribe() branches on cfg.sttApiStyle;
rename helper to transcribeJsonBase64 with provider-neutral error text and a
BadRequestException (400) when the base URL is missing for the JSON style.
- client: SttApiStyle type on IAiSettings/IAiSettingsUpdate; "Request format"
Select on the Voice/STT settings card; i18n.
- ai.service: route *.openrouter.ai STT to its JSON+base64
/audio/transcriptions API; keep the OpenAI multipart path (AI SDK) for
OpenAI/self-hosted whisper. Unify transcription behind transcribe().
- /transcribe controller: surface the real provider/transport reason
(describeProviderError) instead of an opaque 500; preserve HttpException.
- testConnection: add an 'stt' capability (silent-WAV probe) + DTO; client
gets a Test endpoint button and status dot on the Voice/STT card.
- useDictation: log full errors to the console and show the real reason
(mic start + transcription paths); handle NotReadable/Abort and missing
mediaDevices.
- docs(CLAUDE.md): require full error logging + specific user-facing messages.
Add a documentation bullet that enforces comprehensive error logging and user‑facing messages, ensuring caught errors are fully logged and presented with specific, human‑readable explanations rather than generic messages.
The README files now list Voice dictation as a completed feature (✅) instead of an upcoming one (🔭). Consequently, the detailed `voice-dictation-plan.md` documentation has been removed. This reflects that the feature is ready and no longer merely a plan.
Add a section describing how Kysely runs migrations in alphabetical (timestamp) order and the need to verify migration timestamps when merging branches. This helps prevent migration ordering errors and boot failures.
Update the README files to list newly planned features on the roadmap, including page templates, a public‑share AI assistant, and academic‑style footnotes. This improves documentation of upcoming functionality.
Add a detailed design and implementation plan for an AI assistant that
operates on publicly shared document trees. The document outlines the
feature scope, architecture, security considerations, and remaining work,
providing context for future development.
Add docs/page-templates-plan.md describing a whole-page live
transclusion feature: pages flagged is_template, a new pageEmbed
node referencing a source page, a whole-page lookup endpoint reusing
the existing transclusion access-control and share paths, reference
sync, duplicate remap, and cycle/deletion/access/export edge cases.
Decision: separate pageEmbed node over extending transclusionReference.
Add push-to-talk voice dictation that transcribes recorded audio on the
server via the workspace's OpenAI-compatible AI provider (Whisper /
gpt-4o-transcribe / self-hosted whisper), then inserts the text.
Backend:
- New `stt_api_key_enc` column + migration; STT creds parity with chat/
embeddings (sttModel/sttBaseUrl/sttApiKey, write-only key, fallbacks to
chat baseUrl/key). Both provider whitelists updated (service + repo).
- AiService.getTranscriptionModel + AiTranscriptionService.
- Gated POST /ai-chat/transcribe (dictation flag → 403, JWT + workspace
scope + throttle, 25MB cap, MIME whitelist, never logs audio/key).
- New `settings.ai.dictation` workspace flag (DTO + service + audit).
Frontend:
- Wire up the Voice/STT settings card (model/base URL/key) and the
Voice-dictation toggle.
- New `features/dictation`: useDictation (MediaRecorder state machine),
MicButton, transcribe service; integrated into the chat composer and a
new editor-toolbar dictation group, both gated by ai.dictation.
Add a "Cutting a release" subsection to CI / release: version selection
(SemVer), synchronized package.json bump (root + client + server; mcp is
independent), CHANGELOG update, lightweight v-tag, and push that triggers
release.yml. Document moving a misplaced tag via git tag -f + force-push,
and note the git tag (not package.json) is the source of truth for the
displayed version.
Make the floating AI chat window open at a larger default size and
allow stretching it further, plus shrink the fonts.
- ai-chat-window.tsx: DEFAULT_WIDTH 362->540, DEFAULT_HEIGHT 602->680;
clamp the default width to the viewport in computeInitialGeom()
(symmetric with the existing height clamp) to avoid overflow on
narrow screens.
- ai-chat-window.module.css: raise resize caps (max-width 560->900px,
max-height 880->1100px); base font-size 12->11px.
- ai-chat.module.css: chat content font .messages sm->xs.
Add a root CLAUDE.md describing the Gitmost project for Claude Code and
other AI-agent tooling. Covers: the AGPL-only/no-EE fork philosophy and the
"internal identifiers stay docmost" naming gotcha; the pnpm+Nx monorepo
layout; build/lint/test (incl. single-test) and migration commands; and the
big-picture architecture — the two server processes (API on PORT, collab on
COLLAB_PORT via Hocuspocus/Yjs), NestJS module layout, Kysely/Postgres+
pgvector/Redis persistence, the shared editor-ext (client + server; the MCP
package vendors its own schema mirror), and the two AI subsystems (embedded
/mcp server and the CASL-scoped AI chat agent with RAG).