Commit Graph

579 Commits

Author SHA1 Message Date
claude_code
aeea315618 fix(ai-chat): stop duplicating the assistant name in the typing indicator
While a multi-step agent turn is "thinking between tool steps", the
assistant identity name (e.g. the role name) was rendered twice, stacked:
once by the assistant message row (MessageItem) and once by the standalone
TypingIndicator below it. The indicator's name label only makes sense when
it stands in for a not-yet-started assistant row; between steps the row
above already shows the same name.

Render the indicator's dimmed name label only when it is standalone (no
assistant row at the tail yet); otherwise show just the "Thinking…" dots.

- typing-indicator.tsx: optional showName prop (default true); the name
  label renders only when showName !== false
- message-list.tsx: exported typingIndicatorShowsName(messages) helper;
  pass showName to the indicator at the render site
- typing-indicator-shows-name.test.ts: unit-cover the four cases

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:32:20 +03:00
claude code agent 227
7c48bab1f2 test: add unit tests for 10 candidates from issue #139
Adds co-located unit tests for ten targets (client → vitest *.test.ts(x),
server → jest *.spec.ts), plus minimal behavior-preserving extractions/exports
where the issue required a pure function to test:

- encode-wav: WAV header + PCM16 clamping
- editor-ext embed-provider / utils (sanitizeUrl, isInternalFileUrl) / indent
  (export clampIndent)
- label.dto @Matches regex
- move-page.dto vs generateJitteredKeyBetween parity (bug locked via test.failing)
- new-note-button canCreatePage (extracted to can-create-page.ts)
- history-editor diff (extracted pure computeHistoryDiff into history-diff.ts)
- notification getTypesForTab + repo contract (direct-tab divergence locked via
  test.failing)
- search buildTsQuery (extracted + sanitizes operator inputs so adversarial
  queries no longer risk a to_tsquery 500)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 04:13:44 +03:00
claude_code
7884dc2e1a feat(editor): add gitmost bridge for listing spaces/pages and creating a page with a recording
Extend the window.gitmost native-host bridge with three methods that work
when no page is open, registered globally at the app-shell level (not in
page-editor.tsx) so the react-router navigate fn and the api-client are
available:

- listSpaces(): reuse getSpaces() -> [{id, name}], flags truncation.
- listPages({spaceId, parentPageId?}): reuse getSidebarPages()
  -> [{id, title, hasChildren}], first page only (truncated flag).
- createPageWithRecording({spaceId, parentPageId?, title?, base64,
  filename, mimeType}): validate/decode the audio first (so a bad payload
  leaves no junk page), resolve the space slug via getSpaceById (no-space
  probe), createPage(), navigate via the router (no reload), wait for the
  new page's editor to be mounted+editable+Yjs-connected, then run the same
  uploadAudioAction path as insertRecording. Resolve-only error contract:
  no-space | create-failed | editor-timeout | insert-failed.

DRY: extract the base64 decode/validate + audio-insert pipeline from
page-editor.tsx into features/editor/gitmost/gitmost-recording.ts; the
existing insertRecording now delegates to it (behavior unchanged).

Mount GitmostGlobalBridge once in GlobalAppShell. Before navigating, reset
the shared yjsConnectionStatusAtom so the readiness gate waits for the NEW
page's provider to connect instead of a stale "connected" from a previously
open page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:51:17 +03:00
97002f318a Merge pull request 'fix(ai-chat): adopt the server-returned chat id (two-tab adoption race #137)' (#138) from fix/ai-chat-chatid-adoption into develop
Reviewed-on: #138
2026-06-23 03:35:03 +03:00
claude code agent 227
870df458ed test(ai-chat): cover double onTurnFinished; name hook option/result types
Closes the 4th PR #138 review (3 suggestions, no blockers).

- Double-call safety: a failed turn fires both useChat onFinish AND onError, so
  onTurnFinished can run twice in one turn (streamed id, then no id) before a
  re-render. onTurnFinished now reads the live id from a ref (set imperatively on
  primary adoption) instead of the stale closure, so the 2nd no-id call cannot
  re-arm the error-path fallback at the source; the render-phase reconciler is the
  second layer. Added a hook test for the sequence — verified it fails only if
  BOTH layers are removed (non-tautological).
- Conventions: extracted named UseChatSessionOptions / UseChatSessionResult
  interfaces (was an inline param literal + ChatSession); the test derives its
  driver props from them.
- Simplification: extracted the chatIdSnapshot(chats) projection used at both the
  fallback arm site and the resolver effect.

Architecture notes from the review (caller-driven disarm contract; cross-process
{chatId} type) intentionally left as Variant A per the reviewer's recommendation.

tsc clean; 128 ai-chat tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:33:15 +03:00
claude code agent 227
8f9a218c68 fix(ai-chat): disarm pending adoption on New chat/select; drop dead helper
Closes the 3rd PR #138 review.

Warning fix: the render-phase reconciler only disarms the error-path adoption
fallback when activeChatId actually changes. Pressing 'New chat' while ALREADY
in a new chat keeps activeChatId === null (a no-op atom write), so the reconciler
never fired and a stale armed fallback could adopt the just-failed chat from a
late refetch, yanking the user out of their fresh chat. useChatSession now
returns cancelPendingAdoption(); the window calls it from startNewChat AND
selectChat. (The hook call moved above those callbacks so they can reference it.)
Added a hook test that fails without the explicit disarm, plus a test for the
existing-chat onTurnFinished branch (no adoption + per-chat invalidation).

Cleanups: removed the dead pickNewlyCreatedChatId (the fallback effect uses
newlyAddedChatIds directly with the 0/1/>1 decision inline) and its tests/doc
mention; inlined the two invalidation closures (onTurnFinished is read live by
useChat's onFinish, never in an effect dep array, so memoizing them was needless
ceremony).

Verified: tsc clean, 127 ai-chat tests green; live (z.ai glm-5.2) new chat + 2nd
turn recalled the number in the SAME row (1 chat / 4 messages), no page errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:04:40 +03:00
claude_code
a8d1caf039 Merge branch 'feat/ai-settings-per-card-save' into develop
Add a per-card "Save and test" button to the AI provider settings (save the
whole form, then probe the endpoint on success).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:26:21 +03:00
claude_code
6946ee4415 feat(ai-settings): add per-card "Save and test" button
The single global "Save endpoints" button sat far below the fold and the
per-card "Test endpoint" button probed the server-stored settings, so it
ignored unsaved form edits. Replace each endpoint card's "Test endpoint"
button with a combined "Save and test" button that persists the whole form
first and only runs the card's connection probe on a successful save; the
global "Save endpoints" button is kept for save-only.

- Add handleSaveAndTest: save (rethrows on failure) then probe; skip the
  test if the save fails (the mutation already surfaces the error).
- Add savingTestCapability state so only the clicked card spins during the
  shared save while all save controls stay disabled (no concurrent saves).
- Reset the previous probe result when a new save+test starts.
- Add the "Save and test" en-US translation key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:26:13 +03:00
claude code agent 227
f59ca3cb0d refactor(ai-chat): extract useChatSession hook + lock the id lifecycle with tests
Addresses the 2nd PR #138 review (test debt + the Variant-B architecture ask).

The new→persisted chat id lifecycle (mount key, both adoption paths, the
history-load latch, the render-phase reconciler, onTurnFinished) is moved out of
the 768-line window into a new useChatSession hook driven by a pure
threadSessionReducer (reconcile/adopt), so adopt-vs-switch is one explicit
dispatch point and the scattering the review flagged is gone (window: 768→~620).

Tests (the blockers):
- use-chat-session.test.tsx — hook-level locks incl. the #137 regression
  (adopts the authoritative streamed id 'A', NOT chats.items[0]='B' — fails on
  the old heuristic), the error-path fallback (arm/adopt/ambiguous/add+delete),
  the disarm-on-reconcile lock (a fallback armed then switched away must not be
  adopted by a late refetch), in-place-adopt-keeps-key vs external-switch-remount,
  and the waitingForHistory latch.
- extractServerChatId (reading message.metadata.chatId) and newlyAddedChatIds
  extracted as pure helpers with unit tests; threadSessionReducer tested.

Cleanups: single canonical #137 explanation in adopt-chat-id.ts (other sites
reference it); fallback effect computes the set diff once; invalidate callbacks
memoized; redundant invariant tests folded.

Behavior preserved — re-verified live (z.ai glm-5.2): new-chat adopt + 2nd turn
in the same row, no mid-conversation remount, two-tab race leak-free, switch to
an existing chat reseeds full history, reload restores history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:25:52 +03:00
claude_code
10fe605159 feat(editor): expose window.gitmost bridge to insert recordings as audio blocks
Add a stable native-host JS-API on window.gitmost so the gitmost.app
WKWebView wrapper can hand a recorded audio file to the current page as an
audio block without depending on editor internals (atoms/Tiptap/Yjs).

- page-editor.tsx: register/tear down window.gitmost only while an editable
  page editor is mounted (ready=true, version=1). insertRecording validates
  mime/size, decodes base64 in 4-char-aligned chunks, rejects oversized
  payloads before decoding (too-large), reuses the existing uploadAudioAction
  pipeline, resolves machine-readable error codes
  (no-editor/bad-type/too-large/insert-failed) and never throws. Cleanup is
  identity-guarded so an unmounting PageEditor cannot disable a newer live
  registration.
- editor-ext audio-upload: return the uploaded attachment from the upload fn
  so the bridge can report success + attachmentId. Backward compatible:
  existing fire-and-forget callers ignore the return value; the error path
  still swallows and returns undefined (no re-throw).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:24:28 +03:00
claude_code
da058bb6a0 feat(editor): transcribe embedded audio blocks
Add a gated "Transcribe" action to the audio block's bubble menu so an
already-embedded audio file can be transcribed (previously only live
microphone dictation was supported). The button fetches the embedded
file, normalizes its MIME type to the STT whitelist, reuses the existing
POST /ai-chat/transcribe endpoint, and inserts the result as a paragraph
right below the audio block.

- Mount the previously-unwired AudioMenu in page-editor (edit mode only),
  which also surfaces the existing Download/Delete actions for audio.
- Gate the Transcribe button on settings.ai.dictation; show a spinner and
  block double-submits while transcribing; map errors like the mic hook.
- Disambiguate duplicate-src blocks by re-scanning the doc and inserting
  after the audio node closest to the originally selected one.
- Add i18n keys (en-US, ru-RU): Transcribe, Transcribing…, No speech
  detected, plus ru-RU translations for the transcription error messages.
2026-06-23 01:47:34 +03:00
claude code agent 227
ba90147749 refactor(ai-chat): consolidate thread id lifecycle into one atomic identity
Addresses the PR #138 review's architecture note (the deferred 'non-blocking'
item). The brand-new -> persisted chat identity was spread across two separate
useState slots (threadKey + liveThreadChatId) plus a render-phase guard, so the
mount key and the live thread's chat id could in principle diverge.

Consolidate them into ONE atomic state object { key, chatId } with pure,
unit-tested transitions in thread-identity.ts:
- newThread(newKey)       — a brand-new id-less chat (fresh session key);
- switchThread(chatId)    — switch to an existing chat (key := chat id) -> remount;
- adoptThread(prev, id)   — a new chat learns its real id IN PLACE (key unchanged
                            -> no remount, live useChat store preserved).
The 'key vs chatId diverged' state is now unrepresentable. The render-phase
reconciliation stays (the atom is also set externally, e.g. page-history's
history-item opening a referenced chat), but adoption vs switch is now explicit.

Behavior is unchanged; verified live: new-chat adopt + 2nd turn in the same row,
no mid-conversation remount, the two-tab race stays leak-free, switch-to-existing
remounts + reseeds, and reload restores full history. Adds thread-identity.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:46:15 +03:00
claude_code
93c1e6e3e4 refactor(ui): replace comments icon with button
Switched the comments ActionIcon to a Button component, using a text label instead of an icon. This improves clarity and aligns the header menu with the current design guidelines.
2026-06-23 01:31:01 +03:00
claude code agent 227
580f3442b8 fix(ai-chat): prevent duplicate chat row on first-turn error; add adoption tests
Addresses the PR #138 review.

Blocker 1 — duplicate chat row: a brand-new chat whose first turn errors BEFORE
the SSE 'start' chunk never receives the authoritative chatId, so metadata
adoption can't run; a retry then sent chatId:null and the server inserted a
SECOND chat row, orphaning the first turn. Keep metadata adoption as the primary
path (resolveAdoptedChatId) and add a bounded, unambiguous fallback: on a
new-chat finish with no server id, snapshot the known chat ids and, once the
list refetch lands, adopt the SINGLE newly-appeared id (pickNewlyCreatedChatId).
Zero or >1 new ids (e.g. two tabs racing) → no adoption — no items[0] guessing,
so #137 stays fixed. The wait-for-refetch guard compares set membership (robust
to a concurrent delete), and the diff dedupes so a repeated id from a paginated
list never reads as ambiguous.

Blocker 2 — tests: new adopt-chat-id.test.ts covers both pure helpers (adopt
decision + newly-created-id diff incl. dedupe/reorder); the server
messageMetadata callback is extracted to chatStreamStartMetadata and unit-tested
(start -> {chatId}, otherwise undefined).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:17:30 +03:00
claude_code
44a1b5b003 feat(dictation): gate streaming dictation behind a workspace toggle
Streaming (silence-cut) dictation was hardcoded on. Put it behind a per-workspace
flag settings.ai.dictationStreaming, default off, with batch dictation as the
default and fallback. Mirrors the existing settings.ai.dictation flag end to end:

- server: aiDictationStreaming on UpdateWorkspaceDto + workspace.service writes
  settings.ai.dictationStreaming via updateAiSettings (jsonb merge keeps siblings)
- client: IWorkspaceAiSettings.dictationStreaming, an optimistic "Streaming
  dictation" sub-toggle under "Voice dictation" (disabled when dictation is off)
- gate the MicButton streaming prop in the editor toolbar and chat composer on
  the flag instead of a literal true

When the flag is absent/false both call sites pass streaming=false, so the VAD
model/wasm are never fetched and behavior is unchanged. Reuses the existing STT
model and /ai-chat/transcribe — no new provider/model/endpoint fields.

Removes the backlog entry now that it is implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:59:35 +03:00
claude code agent 227
1858a5800d fix(ai-chat): adopt the server-returned chat id, not the newest in the list
A brand-new chat (activeChatId === null) had no way to learn the id of the row
the server created: the SSE stream never returned it, so the client adopted the
NEWEST chat in the per-user list (chats.items[0]). With two tabs open, a second
tab creating a chat at ~the same time made its row the newest, so the first tab
adopted the wrong id — its later turns persisted into the other chat and the
agent rebuilt history from it (commands leaked between chats), while the live UI
still showed the original conversation. (#137)

The server now attaches the authoritative chatId to the streamed assistant
message via the AI SDK messageMetadata on the 'start' part, so it reaches the
client on the first chunk. The client reads message.metadata.chatId in useChat's
onFinish and adopts that id in place (no remount, so the live turn and the
thread's chatIdRef follow the real id and the next turn targets the right chat).
The chats.items[0] guess and the adoptNewChat ref are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:46:50 +03:00
claude_code
2d7f85fccb Merge branch 'develop' of https://gitea.vvzvlad.xyz/vvzvlad/gitmost into develop 2026-06-22 21:14:05 +03:00
86bb2742c7 Merge pull request 'fix(qa): resolve QA-pass issues #122–#134' (#135) from fix/qa-issues-122-134 into develop
Reviewed-on: #135
2026-06-22 21:07:19 +03:00
claude code agent 227
8915a875a2 fix(qa): address PR #135 review notes
- Add the two new strings to en-US locale ('Go to login page', 'Move to
  space') so they aren't missing from the base locale (review note 1).
- Avatar upload: accept any image/* MIME instead of a hardcoded png/jpeg/jpg
  list, so webp/gif/etc. are no longer wrongly rejected client-side while
  genuine non-images still surface the error (review note 2).
- Reindex polling: align the deadline-clearing effect with the refetchInterval
  stop condition (indexed >= total, empty workspace included) so the deadline
  clears promptly instead of waiting out the cap (review note 3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:05:45 +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 agent 227
9e1d057878 fix(qa): resolve QA-pass issues #122–#134
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>
2026-06-22 20:47:40 +03:00
claude_code
c53ce35312 feat(ui): swap AI-chat and comments icons to avoid confusion
The AI chat button used IconSparkles and the page comments button used
IconMessage, which read as visually similar speech bubbles. Replace the
AI icon with IconMessageCircleStar (chat bubble + star) and the comments
icon with IconMessages (overlapping bubbles) so the two are clearly
distinct.

- app-header.tsx: IconSparkles -> IconMessageCircleStar
- page-header-menu.tsx: IconMessage -> IconMessages
2026-06-22 20:34:39 +03:00
claude_code
83c61641c9 fix(ai-chat): prevent error banner from clipping its text
The error banner is a flex child of the chat panel column. Mantine's
Alert root is `overflow: hidden`, which (per the CSS flexbox spec) drops
its automatic min-height to 0, so when the message history fills the
panel the flexbox compressed the banner below its content height and the
overflow:hidden clipped the detail text (e.g. "Please try again.").

Set flex-shrink: 0 on the banner so it always shows its full content; the
scrollable message list absorbs the height pressure instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:22:53 +03:00
claude_code
7ddd0cba05 feat(ai-chat): include in-progress streaming turn in chat export
The "Copy chat" export read only persisted DB rows (messageRows), so an
assistant reply that was still streaming — and the user message that
triggered it — were absent from the export until the turn finished and
the messages query was refetched.

ChatThread now mirrors its live useChat snapshot ({ messages,
isStreaming }) into a parent-owned ref; the effect clears the ref on
unmount so a thread switch can't leak its tail into the next chat.
AiChatWindow.handleCopy computes the not-yet-persisted live tail
(messages whose id is absent from messageRows, only while streaming) and
passes it to buildChatMarkdown as `pending`. buildChatMarkdown appends
pending messages after the persisted rows (continuing the heading
numbering), flags the streaming assistant message with an
"still being generated" note, and reuses an extracted renderMessageParts
helper so persisted and pending rendering stay identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:00:35 +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
f6a4df1b08 fix(editor): stop title input losing characters while typing
The fork's server-authoritative WS redesign rebroadcasts PAGE_UPDATED
(updateOne) title changes to the whole space room including the author.
The author's own client applied that echo to the ["pages", slugId] cache,
which feeds the title prop; the setContent effect then overwrote the
in-progress title with the lagging echo, dropping just-typed characters
and jumping the cursor.

Guard the setContent effect so it skips while the title editor is focused
(and when destroyed): external/echo title updates are applied only when
the field is not being actively edited. Page navigation is unaffected
because TitleEditor remounts per page (key={page.id}) and seeds content
at creation.
2026-06-22 19:20:46 +03:00
claude_code
e423c35676 feat(ai-chat): queue messages typed while the agent is streaming
Previously a message composed while the AI agent was streaming a reply was
silently dropped (the composer early-returned on isStreaming). Now such
messages are queued FIFO and sent automatically once the current turn
finishes cleanly.

- chat-input: submit() enqueues while streaming (via new onQueue prop) and
  sends otherwise; during streaming show a queue Send button (when text is
  present) alongside the Stop button; the textarea stays usable.
- chat-thread: per-conversation queue in local state (mirrored in a ref);
  flush the next message in onFinish ONLY on a clean finish - ai@6 useChat
  fires onFinish from a finally on Stop/disconnect/error too, where the queue
  must be preserved. Pending messages render as removable chips above the
  composer. Queue is cleared on chat switch (parent remount) and survives
  in-place new-chat id adoption.
- queue-helpers: pure FIFO helpers (enqueue/dequeue/removeQueuedById) + tests.
- i18n: add en-US/ru-RU keys (Queue message, Remove queued message,
  Send when the agent finishes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:53:31 +03:00
claude_code
8f01a01122 fix(dictation): start streaming dictation on the first click
The streaming mic button only began recording on the SECOND click. The VAD
library creates its AudioContext inside vad.start() and never resumes it; on the
first click the lazy model load (import + MicVAD.new) ran first, so the context
was created after the user-gesture window expired and started suspended — the
audio worklet never ran, so nothing happened. The second click was fast (model
cached) so the context landed inside the gesture and worked.

Create and resume our own AudioContext synchronously at the top of start()
(inside the click gesture, before the model load) and inject it into MicVAD,
which then does not take ownership of it; it is reused across start/stop and
closed only on unmount. Add a "loading" status so the first click is shown as a
spinner (disabled) while the model loads, which also blocks a confusing second
click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:42:44 +03:00
claude_code
14e26aab70 fix(ai-chat): copy/export the full chat history
The "copy chat" button serialized `messageRows` (persisted rows loaded via
`useAiChatMessagesQuery`), which were incomplete in two ways, so the exported
Markdown dropped messages (e.g. "Messages: 2" for a multi-turn chat).

- Exhaust pagination: `useAiChatMessagesQuery` is a useInfiniteQuery that only
  ever loaded the first page (server page size 50, oldest-first), silently
  truncating longer chats. Add an effect that calls `fetchNextPage()` until
  `hasNextPage` is false. Guard on `isFetchNextPageError` so a failed page fetch
  does not loop on the app's global `retry: false`.
- Re-sync after each turn: `onTurnFinished` invalidated only the chat-list
  query, never the per-chat messages query, so `messageRows` went stale during
  a live session. Also invalidate `AI_CHAT_MESSAGES_RQ_KEY(activeChatId)` so the
  export and token counters reflect the just-finished turn.
- Avoid tearing down the live thread: a render-phase latch (`historyLoadedKeyRef`)
  keeps the history loader gating the FIRST mount only, so the post-turn
  background refetch (which can transiently flip `hasNextPage` for a chat whose
  message count is an exact multiple of the page size) no longer unmounts the
  open thread and loses its in-progress useChat state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:17:06 +03:00
claude_code
373c56c0d3 fix(dictation): cut on ~1.5s silence instead of 0.64s
Streaming dictation sends one transcription request per ended speech segment.
With redemptionMs=640 the VAD cut on every ~0.64s gap, so normal halting speech
fragmented into many segments and flooded /ai-chat/transcribe — tripping the
per-user rate limit even at modest real usage.

Raise redemptionMs to 1500 so a cut only happens on a real sentence/thought
pause (~the "couple seconds" the feature was meant to use). Request count now
tracks actual pauses rather than inter-word gaps; the server throttle is left
unchanged (the earlier limit bump was treating the symptom).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:04:35 +03:00
claude_code
6a85680a7d feat(ai-chat): clarify Authorization header field in MCP server form
Add a description to the "Authorization header" input in the external
MCP server form explaining that the entered value is sent verbatim as
the Authorization HTTP header value (e.g. "Bearer <token>" or
"Basic <base64>"), since there is no implicit Bearer prefix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:45:29 +03:00
claude_code
39ce47a11f feat(client): remove Tavily preset button from MCP server form
Drop the "Use Tavily preset" quick-fill button from the Add server form
and its now-dead supporting code:
- remove the preset JSX block, applyTavilyPreset handler and TAVILY_PRESET
  constant in ai-mcp-server-form.tsx
- drop the now-unused McpTransport import
- remove the unused "Use Tavily preset" i18n key from en-US translations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:41:35 +03:00
claude_code
6b09c43344 Merge branch 'feat/dictation-silence-vad' into develop
Self-host Silero VAD / onnxruntime-web assets under /vad/ so streaming
dictation loads its wasm backend (fixes the 'text/html' MIME runtime error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:19:17 +03:00
claude_code
7093f184b2 fix(dictation): self-host Silero VAD / onnxruntime-web assets
Streaming dictation failed at runtime with "no available backend found /
'text/html' is not a valid JavaScript MIME type": @ricky0123/vad-web 0.0.30
defaults baseAssetPath/onnxWASMBasePath to "./" (relative to the page URL),
so the worklet, Silero model and ORT wasm/mjs were requested against the SPA
catch-all and came back as index.html.

Serve them from a fixed /vad/ instead:
- scripts/copy-vad-assets.mjs copies the 4 runtime assets (vad worklet,
  silero_vad_v5.onnx, ort-wasm-simd-threaded.jsep.{mjs,wasm}) from node_modules
  into apps/client/public/vad/ (gitignored — the ORT wasm is ~26 MB)
- client dev/build scripts run the copy first so the assets are always present
- useStreamingDictation points both path constants at "/vad/"

Verified: dev server serves all four under /vad/ with HTTP 200 and correct
Content-Type (js/wasm, never text/html); tsc clean. Prod (Docker) build runs
the copy step, so dist/vad/* ships in the image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:19:11 +03:00
claude_code
7bcb5ffcca Merge branch 'feat/trash-undo-toast' into develop 2026-06-22 17:08:21 +03:00
claude_code
2bd75edacc feat(page): replace move-to-trash confirm dialog with undo toast
Soft-deleting a page no longer opens a "Move this page to trash?"
confirmation modal. The page is moved to trash immediately and the
"Page moved to trash" toast now exposes an inline Undo action that
restores the page via the existing restore flow.

- Add move-to-trash-notification.tsx helper that builds the toast body
  (status text + Undo button) as a ReactNode, so it can be used from the
  non-TSX page-query module.
- useRemovePageMutation: show the toast with a stable id, 8s autoClose,
  and an Undo handler that hides the toast and triggers restore.
- space-tree-node-menu / page-header-menu: call handleDelete directly and
  drop the now-unused useDeletePageModal usage. Permanent delete keeps its
  confirmation modal.
- useRestorePageMutation: read the live tree from the jotai store at
  execution time and insert via a functional updater, so Undo restores
  child pages correctly even after the originating component unmounted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:07:37 +03:00
claude_code
4f0da42d88 feat(dictation): streaming STT via silence cut (Silero VAD)
Add a lightweight "streaming" dictation mode as a simpler alternative to the
realtime-websocket path: detect speech with Silero VAD (@ricky0123/vad-web),
cut each segment on a pause and POST it to the existing /ai-chat/transcribe
endpoint, so text appears progressively. No server changes.

- new useStreamingDictation hook (same API as useDictation), lazy-loads VAD,
  in-order seq emission, session-epoch guard against stop->start races
- new encodeWavPcm16 util (Float32 -> mono PCM16 WAV, accepted by the server)
- MicButton gains a `streaming` prop; enabled in the editor toolbar and chat
- VAD tuning: redemptionMs 640 / preSpeechPadMs 320 / minSpeechMs 96
- batch dictation kept as the fallback (streaming=false)
- deps: @ricky0123/vad-web@0.0.30, onnxruntime-web@1.27.0

Note: VAD assets load from the library CDN by default; for self-hosted/offline
set VAD_BASE_ASSET_PATH/VAD_ONNX_WASM_BASE_PATH and copy assets to public/vad/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:52:05 +03:00
claude_code
7ce1a24f82 feat(ai-chat): show creation time and origin document in chat list
Each chat row in the AI-chat history now shows a dimmed second line with
how long ago the chat was created and the document it was created in
("N ago / <document>", or "No document" when started outside a page).

Server:
- New migration: nullable ai_chats.page_id (FK pages.id, ON DELETE SET NULL).
- Capture the origin page at chat creation from the client-supplied openPage,
  but validate it first: it must be a real page in the same workspace that the
  user may read (PageAccessService.validateCanView), else null. This keeps the
  "openPage.id is attacker-controllable but harmless" invariant - preventing a
  cross-workspace/cross-space page-title leak and a post-hijack FK crash.
- findByCreator left-joins pages (scoped by workspace, defense-in-depth) and
  returns pageTitle.

Client:
- IAiChat gains pageId/pageTitle; ConversationList renders a ChatMetaLine
  (useTimeAgo + origin document) as a dimmed second line.
- Add i18n key "No document" (en-US, ru-RU).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:16:26 +03:00
claude_code
89ac8fa37b style(dictation): match mic button halo radius to button shape
Update the halo's border-radius from a fixed 50% circle to the theme's default radius variable. This ensures the red pulse follows the button's rounded‑square outline instead of appearing circular.
2026-06-22 16:01:53 +03:00
claude_code
cbd980f6e4 fix(ai-chat): handle browser fetch failures as lost connection
Add detection for browser fetch‑failure messages (e.g., “Failed to fetch”, “Load failed”, “NetworkError”) and return a clear error indicating the streaming connection to the server was lost. Refine the connection‑error regex to avoid overlapping patterns while preserving provider‑side error handling.
2026-06-22 03:49:36 +03:00
claude_code
1c9785997a fix(ai-chat): surface dropped-stream errors clearly + log client disconnects
A mid-stream connection drop showed a generic "Something went wrong / Load
failed" banner and left no server-side trace.

- error-message: classify the browsers' own fetch-failure strings ("Load
  failed" on WebKit, "Failed to fetch" on Chrome, "NetworkError" on Firefox)
  as a lost connection, so the banner names the cause instead of the generic
  heading.
- ai-chat.controller: log a warning in the request close handler when the
  client disconnects before completion, so a drop that reaches the app (e.g. a
  reverse proxy cutting the SSE) is visible in the server logs before the abort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 03:44:25 +03:00
claude_code
2846830bf7 style(home): use neutral gray "New note" button instead of blue primary
The button rendered with Mantine's default blue primary, which clashes
with the app's neutral/dark accent design. Switch both the single-space
and the space-picker variants to variant="light" color="gray".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 03:17:02 +03:00
claude_code
f218852184 Merge branch 'fix/home-new-note-visibility' into develop
fix(home): make the "New note" button visible (role-based writability)
2026-06-22 02:49:17 +03:00
claude_code
93d8c1f775 fix(home): show "New note" button by resolving writability from role
The button never rendered: it filtered spaces to writable ones via a
CASL ability built from membership.permissions, but the /spaces list
endpoint returns membership.role only (permissions come from
/spaces/info). The empty ability hid the button for everyone.

Resolve writability from membership.role instead, mirroring the server
space-ability mapping (ADMIN and WRITER can manage pages, READER is
read-only). Drop the now-unused CASL imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:49:17 +03:00
claude_code
ef74058301 style(editor): align byline dictation mic with the info icon
The byline mic rendered blue and with a smaller (16px) glyph next to the
gray 20px info icon, so it looked misaligned with an uneven gap. Add
optional color/iconSize props to MicButton (forwarded through
DictationGroup) and render the byline mic gray at 20px, wrapping it and
the info icon in a tight nowrap group so they read as a snug, aligned
pair. The AI chat mic is unchanged (passes neither prop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:48:50 +03:00
claude_code
3a3d22ac55 refactor(editor): remove duplicate dictation mic from the fixed toolbar
The dictation button now lives in the always-rendered page byline, so the
copy in the fixed toolbar was redundant: with the toolbar enabled the mic
showed up twice. Drop the DictationGroup render, its isDictationEnabled
guard, and the unused import from the toolbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:33:07 +03:00
claude_code
4281a370b1 Merge branch 'feat/stt-language' into develop
Add dictation language selection to STT settings.
2026-06-22 02:29:13 +03:00
claude_code
a16ef2346f feat(ai/stt): add dictation language selection to STT settings
Add a per-workspace `sttLanguage` setting (ISO-639-1 hint; empty =
auto-detect) and a searchable language picker in the Voice / STT settings
card. The hint is forwarded to the transcription endpoint:
- multipart path via the AI SDK `providerOptions.openai.language`
- JSON (OpenRouter) path via a top-level `language` body field
only when non-empty, so auto-detect behaves exactly as before.

Threaded through the whole stack: ai.types, update DTO, AiSettingsService
(resolve/getMasked/update), the workspace.repo SQL allowlist, the client
ai-settings service types, and the provider-settings form. Adds en-US
source keys and ru-RU translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:29:07 +03:00
claude_code
01d7c2b465 Merge branch 'feature/home-new-note' into develop
feat(home): prominent "New note" button on the dashboard
2026-06-22 02:19:07 +03:00
claude_code
6d0ee6c61f feat(home): add prominent "New note" button to dashboard
Add a big "New note" action to the Home screen that creates a new page
and opens it. Since the home screen has no active space, the target
space is resolved from the user's writable spaces (CASL Manage/Page
gate, mirroring the space sidebar): created directly when there is one
writable space, picked from a dropdown when there are several, hidden
when there are none. Menu items are disabled while a create is in
flight to avoid duplicate pages.

- New component features/home/components/new-note-button.tsx
- Render it at the top of pages/dashboard/home.tsx (above the carousel)
- Add i18n keys "New note" / "Create in space" to en-US and ru-RU

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:18:31 +03:00