The right border on the address prefix read as a stray vertical line
between the domain and the slug. Remove it and rely on the subtle
prefix background alone to separate the two parts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Custom address" slug field sized its leftSection with a
character-count heuristic (label.length * 7 + 12), which over-estimated
the real width of the small dimmed domain prefix and left an ugly empty
gap between "docs.../l/" and the input text.
- Measure the real prefix width via a ref + useLayoutEffect (scrollWidth)
and feed it to leftSectionWidth so the slug sits flush against the
prefix, regardless of host length or font metrics.
- Restyle the prefix as an attached addon: subtle background, a right
divider border and input-matching left corner radii.
- Minor spacing tidy: description mb 4->6, action buttons mt xs->sm.
No behavior change: validation, availability probe, save/remove and the
reassign modal are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a retargetable, human-readable vanity link namespace /l/<alias> that
sits alongside the untouched /share/... routes.
- New share_aliases table (workspace-scoped, UNIQUE(workspace_id, alias),
page_id nullable ON DELETE SET NULL so the address outlives its target).
- ShareAliasRepo + ShareAliasService (create / no-op / 409 reassign guard /
availability / request-time readable-target resolution through the single
existing share boundary).
- Public ShareAliasRedirectController (GET /l/:alias) issues a 302 (never 301,
the target is mutable) to the canonical /share/:key/p/:slug page; unknown /
dangling / no-longer-readable aliases serve the SPA index with no leak.
'l/:alias' excluded from the global /api prefix.
- Authenticated ShareAliasController (set/remove/availability/for-page).
- Shared ASCII-only normalize/validate util (server + client copies).
- Client: Custom address block in the share modal (live normalize + debounced
availability + copy + reassign confirmation dialog).
- Unit tests: util, repo SQL-shape, service semantics, migration/entity sanity
(server jest) + client alias util (vitest).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `subpages` node showed only one level of direct children. Add a `recursive`
attribute that renders the FULL descendant tree of the current page — fully
expanded, unlimited depth. Default `false`, so every previously-inserted node
stays flat (backward compatible). No backend changes: `POST /pages/tree` (via the
`getSpaceTree` wrapper) already returns the whole subtree as a flat `IPage[]`
(recursive CTE, permission-filtered); the nested tree is built on the client by
`parentPageId`.
- editor-ext `subpages.ts`: `recursive` attribute (parse/render `data-recursive`),
shared by client + server so the collab ProseMirror schema keeps the attribute.
- `getSpaceTree`: arg loosened to `{ spaceId?; pageId? }` (the endpoint accepts
either); new `useGetPageTreeQuery(pageId)` react-query hook.
- `subpages-view.tsx`: split into `FlatSubpages` (unchanged) and
`RecursiveSubpages`; `buildSubtree` assembles the nested tree (cycle/self-parent
guard, `sortPositionKeys` per level, root excluded) and a recursive `TreeNode`
renders it (16px indent per depth, soft "showing N" note past 300 — data never
capped). Shared/public context reads the already-nested shared tree, no
`/pages/tree` request.
- toggles: bubble-menu flat⇄tree button + a second slash-menu item "Page tree".
Review follow-ups folded in: invalidate `["page-tree"]` from the create / update /
move / delete cache helpers so an open recursive tree refreshes (no stale data);
mode icon made reactive on editor transactions; `t` threaded into `TreeNode`
(no per-node useTranslation); shared-subtree hook deduped to a thin alias.
editor-ext build + client `tsc --noEmit` both clean. Backend untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch of fixes from the automated QA pass on develop. Each was reproduced and
then verified fixed live (browser/curl); logic-bearing fixes have unit tests.
Functional bugs:
- #122 collab-token was capped by the anonymous public-share-AI throttler (5/min);
skip all non-AUTH named throttlers on this auth-guarded, client-cached route.
- #123 editor onAuthenticationFailed threw `jwtDecode(undefined)` and never
reconnected; read the token via a ref, guard the decode (incl. missing exp),
and refetch+reconnect on any auth failure.
- #124 a slash command containing a space ("/Heading 1") inserted literal text;
enable allowSpaces and close the menu when the query matches no items.
- #125 space slug auto-gen produced uppercase initials for multi-word names;
computeSpaceSlug now yields a lowercase alphanumeric slug.
- #126 AI chat window position/size now persisted (atomWithStorage) across reload;
also fixes a latent ResizeObserver-attach bug on first open.
- #127 workspace name update accepted URLs; add @NoUrls (parity with setup).
- #132 icon-columns 4/5 passed calc() into SVG width/height attrs (console spam);
size via style. share-for-page query returns null instead of undefined.
- #134 "Reindex now" counter looked stuck: reindex runs async; the client now
polls coverage (bounded) so the counter climbs live; misleading server comment
reworded.
UX / consistency:
- #128 add success toasts to favorite/label/avatar/member-(de)activate.
- #129 "1 result found" pluralization; hide the single-option Type filter.
- #130 replace raw Zod strings with friendly messages (name/password/group).
- #131 unify "Untitled" casing in tree/breadcrumb/tab; stop force-uppercasing
space-name chips; fix confirm-dialog labels (Cancel / Remove), invite
placeholder typo, Export/Move-to-space labels.
- #133 disable profile Save when clean; toast on unsupported avatar image;
style the invalid-invitation page with a CTA; hide Share for read-only users;
align the dictation "not configured" message; "Go to login page" typo.
Tests: computeSpaceSlug, workspace-name NoUrls DTO, share-query null
normalization, slash getSuggestionItems empty-close.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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).
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>
The widget hardcoded a generic 'Something went wrong' body and ignored
error.message, violating AGENTS.md. Render describeChatError(error.message, t) —
the same helper the internal chat uses — so a reader sees the real 402/429/503
cause instead of a bare 'try again'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Anonymous public-share AI assistant:
- Add a workspace setting `publicShareAssistantRoleId` so an admin can pick which
agent role (identity/persona) the anonymous assistant adopts. The role's
instructions REPLACE the built-in persona while the immutable safety framework
is still always appended; the role's optional model override takes precedence
over the cheap publicShareChatModel. Resolved server-authoritatively
(workspace-scoped, soft-delete aware; disabled/missing roles fall back to the
built-in persona, so the tool scope remains the real security boundary).
- Plumb the field through the update DTO, ai-settings service, the workspace.repo
ALLOWED whitelist, resolve()/getMasked(), stream-time role resolution and the
prompt/model, plus the settings UI: a new "Assistant identity" Select listing
enabled roles (and surfacing a saved-but-disabled role explicitly).
Public-share branding / floating icon:
- Fix the AI assistant FAB overlapping the "Powered by ..." button (both were
Affixed bottom-right): stack the FAB above the bottom-right branding.
- Rename "Powered by Docmost" -> "Powered by Gitmost" and point the link at the
gitmost repo.
Tests: extend public-share-chat.spec (role persona replacement still appends the
safety framework, resolveShareRole edge cases, model-override precedence).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts with the independently-merged ai-agent-roles feature:
- ai-chat.module.ts: keep BOTH AiAgentRolesModule and the public-share
wiring (Share/Search modules, PublicShareChatController, services).
- ai.service.ts: take develop's getChatModel ChatModelOverride superset,
which already covers the public-share model-id-only override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Lets an unauthenticated viewer of a published share ask an AI scoped strictly
to that share's page tree. The authenticated agent is untouched; the security
boundary is the tool scope (no identity), and nothing is persisted.
Server:
- workspace toggle settings.ai.publicShareAssistant (default off) +
optional settings.ai.provider.publicShareChatModel (cheap model id; reuses
the chat driver/baseUrl/key). getChatModel(workspaceId, override) substitutes
only the model id, falling back to chatModel.
- POST /api/shares/ai/stream (@Public, SSE). Guardrail funnel, each failing
before streaming: toggle off -> 404; share missing/wrong-workspace/sharing
off -> 404; pageId not in share tree -> 404; provider unconfigured -> 503;
per-IP (5/min) and per-workspace (300/h, IP-independent) rate limits -> 429.
Uniform 404s never confirm a private page's existence.
- forShare read-only in-process toolset: searchSharePages (existing shareId
FTS branch, no spaceId/userId), getSharePage (getShareForPage gate +
share.id check, content via the public sanitizer), listSharePages. No write/
comment/history/cross-space/external-MCP tools.
- Locked share system prompt + immutable safety block; stepCountIs(5).
- /shares/page-info exposes an aiAssistant flag (gated behind isSharingAllowed).
Client: an ephemeral, text-only Ask-AI widget on the public shared page,
shown only when the flag is set; useChat -> /api/shares/ai/stream,
credentials omit. Admin toggle + model field in Settings -> AI.
Also adds a jest moduleNameMapper for src/-rooted imports (fixes pre-existing
unresolvable specs; additive).
Implements docs/public-share-assistant-plan.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Strip the proprietary client EE so the fork ships a clean community/AGPL
edition, mirroring Forkmost. Delete apps/client/src/ee (201 files) and
packages/ee, and patch every consumer that imported from @/ee/*.
- gate-out EE features (useHasFeature -> false): API keys, SSO, MFA, SCIM,
audit logs, AI / AI-chat, templates, page permissions, page verification,
comment resolution, trash retention, viewer comments
- drop cloud/billing/trial/entitlement/posthog flows; sign-in is now
email+password only (no SSO/LDAP/cloud)
- remove EE routes from App.tsx and EE entries from sidebars/settings nav
- restore the community page-share button (ShareModal) that the EE
PageShareModal used to provide
- remove the dead "Attachments" search filter, dead MFA navigation and
orphaned route constants
Client type-checks clean; full `pnpm build` is green for all three projects.
* fix(editor): hide transclusion borders and reset spacing in read-only mode
* feat(share): add full width toggle for shared pages
* feat(share): support resizing sidebar on shared pages
* fix: auto redirect if there is only one SSO provider.
- fix tighten sso redirect
- fix share tree margin
* sync
* package overrides
* feat(tree): replace react-arborist with custom tree implementation
* feat(tree): keyboard arrow navigation between rows
* feat(emoji-picker): focus search input on open
* refactor(emoji): switch to @slidoapp/emoji-mart fork for accessibility
* feat(tree): Home/End and typeahead keyboard navigation
* feat(tree): roving tabindex and * to expand sibling subtrees
* feat(tree): Space activation and ARIA refinements
* fix(tree): move treeitem role to focusable row + aria-current
* add cursor pagination function
* support custom order modifier
* refactor returned object
* feat(db): migrate paginated endpoints to cursor-based pagination
* sync
* support hasPrevPage boolean
* feat(client): migrate pagination from offset to cursor-based
* support beforeCursor/prevCursor
* wrap search results in items array for API consistency
* fix tree nodes sort
* remove comment mark in shares
* remove clickoutside hook for now
* feat: search in shared pages
* fix user-select
* use Link
* render page icons