chore(release): v0.9.0#149
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Notebooks now sync before notes in syncNow() to ensure note-notebook dependencies are satisfied. Adds pullNotebooks/pushNotebooks methods and applyRemoteNotebookChange for bidirectional notebook sync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move validateNotebookTree from inline test definition to a shared module so it can be reused by the API route and other consumers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add conflict state to SyncStatusIndicator with amber warning icon and count. Conflicts now take priority over idle state so users discover them without navigating to Settings. Also export ConflictResolver from sync components barrel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DatabaseConnection.transaction() already calls the inner fn — no need for extra () at call site. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix pullNotebooks() to only advance cursor to last successfully applied change (prevents skipping failed changes on retry) - Fix tree validation snapshot to properly exclude deleted notebooks (prevents ghost parent references in validation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add bidirectional notebook sync
test: add sync-core unit tests (62 tests)
feat: surface sync conflicts in status indicator
# Conflicts: # apps/desktop/src/main/services/apiClient.ts # apps/desktop/src/main/services/syncService.ts # packages/api/src/db/schema.ts # packages/api/src/routes/sync.ts # packages/storage-sqlite/src/migrations/index.ts
feat: add bidirectional tag sync
Configure automated code review with path-specific instructions for core, storage, desktop, and API packages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ration Add optional metadata (name, version, priority) to registerRemarkPlugin and registerRehypePlugin signatures for debugging and execution ordering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 — Backend fixes: - Fix auth middleware: all jose error types return 401 JSON (not 500) - Document all 24 API endpoints in api-reference.md Phase 2 — Sync stability: - Add sync error propagation to renderer via IPC status events - Add exponential backoff on auto-sync failures (cap 5min) - Auto-stop sync on 401 with auth-expired event - Abort in-flight sync operations on logout via AbortController - Token refresh returns typed errors (expired/network/device_limit) - Sync onboarding prompt after 5 notes (session-dismissable) - Offline queue visibility in sidebar footer (pending count) Phase 3 — AI Commands (Cmd+K v1): - Add 'ai' command category with toggle-panel, summarize, rewrite, tweet - Remap Cmd+K to AI panel, insert-link to Cmd+Shift+K - Integrate AiPanel into App layout as right-side panel - Add AI Settings section (API key, model selector, context notes) - Settings schema v2 with v1→v2 migration - Wire existing ai-assistant package (Claude client, RAG, prompts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4 — AI Knowledge (Cmd+K v2):
- Add RAG context fetching: queries search relevant notes automatically
- Add ai:ask-notes command (Cmd+Shift+K) with dedicated system prompt
- Auto-include related notes as context when a note is selected
- Mode toggle in AI panel (Chat vs Ask Notes)
- Context badge showing number of notes used as RAG context
- ASK_NOTES_SYSTEM_PROMPT instructs AI to cite note titles
Phase 5 — AI Extensibility:
- Add AiCommandDefinition type with prompt templates ({{selection}}, {{note}}, {{title}})
- Add AiCommandPreset type for shareable command collections
- Add resolveTemplate(), validateAiCommandDefinition(), validateAiCommandPreset()
- Add aiCommandStore (Zustand vanilla) for plugin-registered AI commands
- Add registerAiCommand() to PluginContext API
- Add IPC handlers for preset import/export (file dialog)
- Add preset management UI in Settings > AI Assistant
- 28 tests for command types, validation, and serialization
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prepare release/0.9.0 with version bumps in root and desktop package.json files, and update CHANGELOG with all changes since 0.1.2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughVersion 0.9.0 adds desktop auth/sync robustness (queued deep-link tokens, sync status broadcasting, backoff, abortable sync), an AI command/preset ecosystem (types, store, plugin registration, AI panel + settings + presets), and a large web marketing redesign with a new UI component library and design tokens. Changes
Sequence Diagram(s)sequenceDiagram
participant OS as OS (deep link)
participant Main as Electron Main
participant Sync as SyncService (Main)
participant Renderer as Renderer Window
participant IPC as IPC / Preload
OS->>Main: open-url with token
alt main window ready
Main->>IPC: deliver token via auth:verify-token
IPC->>Renderer: auth:verify-token event
Renderer-->>IPC: verified result
else renderer not ready
Main->>Main: queue token (pendingAuthToken)
end
Note over Sync,Main: Sync status lifecycle
Sync->>Main: emit sync:status-changed (sync-start / success / error / auth-expired)
Main->>IPC: broadcast sync:status-changed to all windows
IPC->>Renderer: sync:status-changed
Renderer-->>IPC: optionally request sync:pendingCount
IPC->>Main: sync:pendingCount handler
Main->>Sync: return pending count
Main-->>IPC: {success,count}
IPC-->>Renderer: {success,count}
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
Add Phases 6-12 strategic roadmap covering workspace enhancement, command palette evolution, AI workflows, knowledge retrieval, and developer workflows. Remove tracked ._* resource fork files from exFAT volume. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 55
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/web/app/(marketing)/faq/page.tsx (1)
54-61:⚠️ Potential issue | 🟠 MajorFAQ content contradicts new product features introduced in this release.
The FAQ states:
- "Can I use plugins? No." (lines 54-57)
- "Does Readied have AI features? No." (lines 59-61)
However, this release (0.9.0) introduces:
- AI Commands (Cmd+K v1/v2), RAG support, AI knowledge features
- Plugin API, theme system, plugin inspector, CLI plugin commands
These answers need to be updated to reflect the actual product capabilities, otherwise users will be misled.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(marketing)/faq/page.tsx around lines 54 - 61, Update the FAQ entries in the FAQ array inside page.tsx so they no longer contradict the 0.9.0 release: change the "Can I use plugins?" answer to acknowledge plugin support (mention Plugin API, theme system, plugin inspector and CLI plugin commands) and change the "Does Readied have AI features?" answer to acknowledge AI capabilities (mention AI Commands/Cmd+K v1/v2, RAG support and AI knowledge features), and keep phrasing concise and user-facing; locate the entries by matching the question strings "Can I use plugins?" and "Does Readied have AI features?" and replace their answer values accordingly.apps/desktop/src/renderer/components/ai/AiPanel.tsx (1)
245-247: 🧹 Nitpick | 🔵 TrivialConsider using stable message IDs instead of array indices.
Using array index as key (
key={i}) works for append-only lists but can cause subtle issues with React's reconciliation if messages are ever removed or reordered. Consider adding a unique ID to each message when created.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/renderer/components/ai/AiPanel.tsx` around lines 245 - 247, The list is using the array index as the React key which can break reconciliation; change the message model to include a stable unique id (e.g., add an id field when messages are created in the code paths that push messages such as sendMessage/addMessage) and use that id as the key in the render (replace key={i} in messages.map(... <AiMessage ... />) with key={msg.id}); generate ids with a reliable generator (nanoid/uuid or a timestamp+counter) and update any types/interfaces that describe messages to include the id field.apps/desktop/src/main/services/syncService.ts (1)
517-627:⚠️ Potential issue | 🟠 MajorNot every note-sync failure reaches the new status/error bookkeeping.
pull()failures return directly from inside thetry, and notepush()failures are only logged before this method resets error state and emitssync-success. That means the main note-sync path can misssync-error/auth-expiredentirely on pull, or report success after a failed note push.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/main/services/syncService.ts` around lines 517 - 627, The pull() failure path and the push() error branch currently return or log without updating the sync error bookkeeping, so update both places to set this.state.lastError (use pullResult.error or pushResult.error), increment this.state.consecutiveFailures, call this.noteRepository.completeSyncHistoryEntry with an 'error' status if not already done, and emitStatus with type 'sync-error' (or 'auth-expired' if the error indicates auth) and an appropriate error field, then return early instead of continuing to the success flow; do this for the pull failure block where pullResult.success is false and the push failure branch where pushResult.success is false, and ensure you do not call resetAutoSyncInterval() or clear lastError/consecutiveFailures in these error paths so the error/backoff bookkeeping remains correct.packages/ai-assistant/src/rag.ts (1)
41-60:⚠️ Potential issue | 🟠 MajorAsk-notes mode still replays non-note assistant context.
Line 60 appends the full
historyeven whenmode === 'ask-notes'. That means a follow-up can be grounded in an earlier assistant reply instead of the retrieved notes, which breaks the new mode's notes-only contract. At minimum, strip assistant turns before building the message list.Proposed fix
export function buildRagPrompt(input: RagInput): RagOutput { const { query, currentNote, relevantNotes, history = [], mode = 'chat' } = input; + const promptHistory = + mode === 'ask-notes' ? history.filter(message => message.role === 'user') : history; // Build system prompt with context — use knowledge-focused prompt in ask-notes mode let system = mode === 'ask-notes' ? ASK_NOTES_SYSTEM_PROMPT : SYSTEM_PROMPT; @@ - const messages: ClaudeMessage[] = [...history, { role: 'user', content: query }]; + const messages: ClaudeMessage[] = [...promptHistory, { role: 'user', content: query }];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai-assistant/src/rag.ts` around lines 41 - 60, The messages array currently includes the full history regardless of mode, so when mode === 'ask-notes' filter out assistant turns before building messages: replace usage of history in the messages construction with a filteredHistory = history.filter(m => !(mode === 'ask-notes' && m.role === 'assistant')) (or equivalent) and then set messages: ClaudeMessage[] = [...filteredHistory, { role: 'user', content: query }]; ensure you reference the existing variables input.mode (or mode), history, and messages/ClaudeMessage to locate where to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/desktop/src/main/services/apiClient.ts`:
- Around line 235-240: The default branch currently calls
tokenStorage.clearTokens() and treats any non-401/403/5xx refreshResult as
logout-worthy; instead, only clear tokens and throw ApiError for explicit
unauthorized statuses (401/403) or definitive permanent failures, and for
transient statuses (e.g., 408, 429, network timeouts in refreshResult.status)
treat them as retryable: do NOT call tokenStorage.clearTokens(), return/throw an
ApiError or a specific transient error that preserves tokens and signals retry,
and include refreshResult.message in the ApiError; apply the same change to the
other refresh-handling block that references tokenStorage.clearTokens() around
the 340-352 region so transient refresh statuses no longer log the user out.
- Around line 215-219: The 401 handling in request() calls refreshAccessToken()
and on success recursively calls request() without recording that a refresh was
already attempted, allowing infinite refresh/recursion; modify request(endpoint,
options, retryCount = 0) to use the existing third parameter as a retry counter,
only attempt refresh and retry when response.status === 401 and retryCount ===
0, and when retrying call this.request<T>(endpoint, options, retryCount + 1); if
retryCount > 0 and a 401 occurs, surface the auth error (don't refresh again).
In `@apps/desktop/src/main/services/syncService.ts`:
- Around line 457-464: checkAborted() currently polls abortController but API
calls in the sync cycle don't receive a signal, so a logout abort is misreported
as a sync failure; update ApiClient.request(...) to accept an optional
AbortSignal and thread that signal into every apiClient.* call made during the
sync cycle (the calls invoked around checkAborted() and the sync phases), then
when an AbortError or signal.aborted is observed treat it as a cancellation:
rethrow or return a distinct cancellation result and ensure the catch path does
not increment the failure counters or emit the 'sync-error' event for aborts
(instead perform any cleanup and exit gracefully). Ensure you reference and use
the existing abortController.signal and checkAborted() semantics so other logic
still detects aborts.
- Around line 84-91: The isAuthError helper currently inspects only
error.message text and will miss ApiError instances that carry a 401 in their
statusCode; update isAuthError(error: unknown) to detect ApiError by checking
for an object with a numeric statusCode === 401 (in addition to the existing
message checks) so that ApiError(401, ...) triggers auth-expired; reference the
isAuthError function and ensure the check runs before the message-based checks
to safely handle non-Error/ApiError values.
In `@apps/desktop/src/renderer/App.tsx`:
- Around line 639-646: aiConfigCache is only initialized on mount (useEffect
calling window.readied.pluginConfig.getAll) and never updated, so runtime config
changes are ignored; update aiConfigCache.current whenever the AI config changes
by either subscribing to plugin config changes (use the pluginConfig
change/subscribe API, e.g., window.readied.pluginConfig.onChange / onDidChange /
subscribe if available) and updating aiConfigCache.current inside that handler
with proper cleanup in the effect, or reload the config each time the AI panel
opens by calling window.readied.pluginConfig.getAll again when the panel-open
handler (or isOpen state for the AI panel) runs; ensure you reference and update
aiConfigCache.current and remove the subscription on unmount to avoid leaks.
- Around line 24-25: Reorder the type imports in App.tsx so external package
types come before local types: move the AiPanelMode type import from
'@readied/ai-assistant' to the package/type imports group (above the local
preload type import like anything from '../preload/index'), keeping the AiPanel
component import where it belongs; this satisfies the linter's import ordering
rule while preserving the existing symbols AiPanel and AiPanelMode.
In `@apps/desktop/src/renderer/components/ai/AiPanel.tsx`:
- Around line 64-69: The current fallback uses aiSettings.apiKey as the gate for
whether to prefer aiSettings values vs getConfig, which prevents using stored
API key while still preferring plugin config for model/limits; update the logic
in AiPanel.tsx so each setting (model and maxContextNotes) individually prefers
a defined value from aiSettings if present, otherwise falls back to
getConfig(...) and then a hard default — change the expressions that set model
and maxContextNotes (currently checking aiSettings.apiKey) to check
aiSettings.model and aiSettings.maxContextNotes respectively (or use explicit
null/undefined checks) and retain the existing getConfig defaults.
In `@apps/desktop/src/renderer/components/sidebar/Sidebar.tsx`:
- Around line 22-23: The lint warning is caused by import order; move the
external imports for EnableSyncModal and useSyncOnboarding (symbols:
EnableSyncModal, useSyncOnboarding) above the local Sidebar-related imports (the
./Sidebar* block) in Sidebar.tsx so third-party/parent imports appear before
local/module imports; reorder the two lines accordingly and keep existing
relative paths and named imports unchanged.
In `@apps/desktop/src/renderer/components/sync/index.ts`:
- Line 9: The exported symbol EnableSyncModal is re-exported from a file named
LoginModal which causes a naming mismatch; rename the source file LoginModal.tsx
to EnableSyncModal.tsx (or alternatively change the export to match the file)
and update any imports referencing LoginModal to use EnableSyncModal so the
module name matches the primary exported component (refer to the re-export line
exporting EnableSyncModal and the source component defined in LoginModal).
In `@apps/desktop/src/renderer/components/sync/LoginModal.tsx`:
- Around line 57-69: The delayed reset in the useEffect that watches isOpen can
race if the modal is reopened within 200ms; store the timeout ID in a ref (e.g.,
timeoutRef) when you call setTimeout in the branch that runs when !isOpen, and
clearTimeout(timeoutRef.current) whenever the modal opens (isOpen becomes true)
and in the effect cleanup to prevent the pending timeout from later calling
setStep, setEmail, setError, and setResendTimer on a reopened modal; update the
useEffect to set timeoutRef.current = setTimeout(...) and ensure you clear it on
open and on unmount.
In `@apps/desktop/src/renderer/hooks/useSyncOnboarding.ts`:
- Around line 21-24: The inline assertion on counts is unsafe; update the
useNoteCounts hook to return a well-typed shape (e.g., { active?: number } or a
Result type) or add a narrow type guard before reading active so totalNotes is
computed safely; locate useNoteCounts and its call site in useSyncOnboarding.ts
(where counts and totalNotes are defined) and either adjust the hook's return
type signature or implement a small guard (e.g., check counts is non-null and
has an active number) and then compute totalNotes from that guarded value
instead of using (counts as { active?: number }).
In `@apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx`:
- Around line 154-167: The loop registers preset commands unconditionally using
id `preset:${cmd.id}`, causing duplicates on re-import; before calling
aiCommandStore.getState().register(...) compute the registrationId
(`preset:${cmd.id}`) and check the store for an existing command with that id
(e.g. via a suitable getter on aiCommandStore.getState() or by searching the
current commands collection) and skip registration if found; update the loop
around preset.commands in AiSection.tsx to only call register when the id is not
already present.
- Around line 14-18: Reorder the import statements so external package imports
come first, then local project imports, then style imports: move the package
imports (aiCommandStore, AiCommandRegistration from '@readied/plugin-api' and
validateAiCommandPreset, serializePreset, AiCommandPreset from
'@readied/ai-assistant') above the local import for '../../../stores/settings'
and keep the styles import (styles from './Section.module.css') last; ensure the
exact symbols aiCommandStore, AiCommandRegistration, validateAiCommandPreset,
serializePreset, AiCommandPreset, and styles are preserved and only their import
order is changed to satisfy the linter.
- Around line 44-47: The modelOptions array contains outdated Anthropic model
IDs; update the value fields in modelOptions (the entries with value
'claude-sonnet-4-20250514' and 'claude-opus-4-20250514') to the current IDs
'claude-sonnet-4-6' and 'claude-opus-4-6' respectively, and optionally replace
the hardcoded modelOptions (or add a fallback) by fetching available models from
Anthropic at runtime so the list remains current rather than being hardcoded.
In `@apps/desktop/src/renderer/stores/syncStore.ts`:
- Around line 245-284: Validate the incoming event from
window.readied.sync.onStatusChange before casting to SyncStatusEvent: check that
raw is an object and has a string .type, and if not log a warning via
console.warn or a logger including the raw payload; then proceed to switch on
the validated event.type (still using SyncStatusEvent shape) so the existing
cases (sync-start, sync-success, sync-error, auth-expired) remain unchanged;
also add a default switch branch that logs an unexpected event.type (and
optionally the whole event) to aid debugging and avoid silent failures—refer to
window.readied.sync.onStatusChange, SyncStatusEvent, set, and
get().refreshPendingCount when making the changes.
In `@apps/desktop/src/renderer/styles/ai-panel.css`:
- Around line 33-41: The CSS uses a hardcoded RGBA color in the
.ai-panel-context-badge background (and the other occurrence with the same rgba
at the bottom of the file); replace that hardcoded value with a design token
such as var(--accent-muted) (or create a new token like --ai-panel-accent-muted)
and update the background property on .ai-panel-context-badge (and the other
selector that repeats the rgba) to use the token, providing a fallback if
desired (e.g., var(--accent-muted, rgba(94,234,212,0.1))). Ensure the new token
is defined in your global variables so theming remains consistent.
In `@apps/desktop/src/renderer/styles/global.css`:
- Around line 1507-1519: The fallback blue in the .sidebar-sync-prompt
background uses rgba(59, 130, 246, 0.08) which conflicts with the project's
teal/cyan accent; update the fallback for --accent-subtle to match the accent
palette (e.g., rgba(94, 234, 212, 0.08)) so the rule background:
var(--accent-subtle, rgba(94, 234, 212, 0.08)); in the .sidebar-sync-prompt CSS
reflects the expected accent color when the CSS variable is undefined.
- Around line 1529-1556: The current CSS uses fragile positional selectors
.sidebar-sync-prompt-actions button:first-child and .sidebar-sync-prompt-actions
button:last-child; change to explicit class-based selectors by adding semantic
classes on the buttons in the markup (e.g., btn-primary / btn-secondary or
.sync-confirm / .sync-cancel) and update the CSS to target
.sidebar-sync-prompt-actions .sync-confirm and .sidebar-sync-prompt-actions
.sync-cancel instead of :first-child/:last-child; keep the same style rules
(background, color, padding, hover states, etc.) but move them to the new
selectors and remove the positional rules so button order changes won't break
styles.
In `@apps/web/app/`(marketing)/auth/verify/AuthVerifyContent.tsx:
- Around line 42-43: Replace hardcoded hex color classes in the
AuthVerifyContent component with Tailwind semantic utilities: change occurrences
of text-[`#f4f4f5`] to text-zinc-100 and text-[`#a1a1aa`] to text-zinc-400 in the
JSX elements (e.g., the <h1> and <p> elements shown and other instances at the
noted ranges), and update any similar className usages elsewhere in this file so
styling is consistent with existing classes like bg-accent and text-white; if
your project uses custom Tailwind tokens, map these hex values to the
appropriate custom names in tailwind.config.js and use those tokens instead.
- Line 14: The deep link uses a raw token which can contain URL-reserved
characters; update AuthVerifyContent (where window.location.href =
`readied://auth/verify?token=${token}` and the manual button handler around
lines 87-92) to URL-encode the token with encodeURIComponent before
interpolating it into the readied:// URL, and apply the same encoding wherever
token is appended to the deep link to ensure well-formed links for both
auto-redirect and manual click.
In `@apps/web/app/`(marketing)/pricing/page.tsx:
- Around line 30-32: The code reconstructs display prices (monthlyPrice,
annualPrice) and hardcodes formatting for NumberTicker, bypassing the package
product-config single source of truth; update product-config's facade (used via
proPricing) to expose display-ready strings or a currency/format metadata object
(e.g., displayPrice, formattedAmount, currencyCode, intervalLabel) and change
usages in this component (references: monthlyPrice, annualPrice, proPricing, and
NumberTicker) to consume those display fields instead of dividing amountCents
and hardcoding "$", "/mo", "/year", and decimalPlaces; also update the other
affected block (the section around the current 112–135) to use the same
facade-provided display values.
In `@apps/web/app/docs/`[[...slug]]/page.tsx:
- Around line 5-28: The mdx component registry is duplicated as the local
mdxComponents constant; instead import and reuse the shared mapping exported
from the central mdx-components module (merge with defaultMdxComponents if
necessary) and remove the local duplicate registry. Replace the local
mdxComponents definition with an import (e.g., import { mdxComponents as
sharedMdxComponents } from '...') and compose any local additions by spreading
sharedMdxComponents (and defaultMdxComponents only if not already included) so
the page uses the single source of truth (refer to the mdxComponents constant
and defaultMdxComponents symbols to locate where to change).
In `@apps/web/app/docs/layout.tsx`:
- Around line 1-4: Move the type import for ReactNode so it satisfies the
linter: locate the import line "import type { ReactNode } from 'react';" and
place it before the local alias imports (the lines importing source from
'@/lib/source' and baseOptions from '@/lib/layout.shared'), keeping external
package imports (like 'fumadocs-ui/layouts/docs') ordered appropriately; this
reorders the imports to have the React type import precede local project imports
and resolves the lint warning.
In `@apps/web/app/globals.css`:
- Around line 27-46: Replace duplicated hex literals in the Fumadocs overrides
by referencing the existing theme tokens instead of re-declaring values; update
each --color-fd-* variable (e.g., --color-fd-background, --color-fd-primary,
--color-fd-muted, --color-fd-card, --color-fd-ring, etc.) to use the
corresponding theme CSS variables (for example var(--color-background),
var(--color-accent), var(--color-muted), var(--color-foreground)) so the
Fumadocs namespace points to your single source of truth and avoids literal
duplication.
- Around line 263-276: The reduced-motion rule should avoid forcing a non-zero
resting transform and must ensure fade-in elements become visible: change the
shared selector's transform: none !important to transform: initial !important so
existing resting transforms aren't clobbered, and add an explicit override for
.animate-fade-in-up (e.g., .animate-fade-in-up { animation: none !important;
transform: initial !important; opacity: 1 !important; }) so elements that start
with opacity: 0 are visible when animations are disabled.
- Around line 56-80: Replace hardcoded hex colors and scattered !important flags
with the project's design tokens by switching the color values in the selectors
.fd-sidebar [data-active='true'], nav[data-fumadocs], pre:has(code) and
:not(pre) > code to the corresponding CSS custom properties from `@theme` (e.g.,
--color-violet-500 / --bg-code / --muted-border, etc.), remove !important where
specificity can be resolved normally, and if any !important must remain to
override Fumadocs, keep it but replace the literal color with the variable and
add a short explanatory comment above that rule noting why !important is
required.
- Around line 192-210: The animation utilities reference undefined variables
(--gap, --duration, --speed); update the keyframes and utility rules to use CSS
fallbacks (e.g., transform: translateX(calc(-100% - var(--gap, 1rem)));
transform: translateY(calc(-100% - var(--gap, 1rem))); and animation: marquee
var(--duration, 5s) linear infinite; animation: marquee-vertical var(--duration,
5s) linear infinite;) or alternatively define sensible defaults on :root (e.g.,
--gap: 1rem; --duration: 5s; --speed: 1) so `@keyframes` marquee, `@keyframes`
marquee-vertical, `@utility` animate-marquee and `@utility` animate-marquee-vertical
will always have fallback values.
In `@apps/web/components/FaqAccordion.tsx`:
- Around line 39-43: The key and Radix AccordionItem value are derived from
truncated question text causing potential collisions; update the items.map usage
so both the React key and the value include a stable unique identifier (e.g.,
append the loop index or preferably item.id if available) instead of relying
only on question text and its 40-char slice; ensure the value string built for
AccordionItem (the template starting with `faq-...`) and the key both
incorporate that unique id so each AccordionItem has a collision-free
identifier.
In `@apps/web/components/Footer.tsx`:
- Around line 110-128: The Connect column and the bottom icon row both map over
socialLinks, causing duplicate focusable links; update the rendering so only one
variant is focusable per breakpoint by either: 1) applying responsive visibility
classes to the two containers (e.g., show the full "Connect" text list at sm+
and hide the bottom icon row at sm+, or vice versa), or 2) conditionally
rendering one variant based on viewport size; locate the two render sites that
map over socialLinks (the "Connect" column and the bottom icon row) and adjust
their container classes or render logic so only one set of links is
present/focusable at any given breakpoint (alternatively mark the hidden set
aria-hidden="true" and remove from tab order with tabIndex={-1} if keeping DOM
nodes).
In `@apps/web/components/landing/Hero.tsx`:
- Around line 171-183: The backdrop currently uses motion.div with role="button"
and closes on Enter/Space; change the semantics to a dialog by replacing
role="button" with role="dialog" and add aria-modal="true" (and aria-labelledby
or aria-label on the same element referencing the modal title) on the same
motion.div; update the onKeyDown handler in the motion.div to only respond to
Escape (remove Enter/Space handling) so only Escape triggers
setIsVideoOpen(false); keep the onClick to close the modal but ensure focus
management is handled elsewhere (focus trap or return focus) rather than relying
on Enter/Space on the backdrop.
In `@apps/web/components/landing/Testimonials.tsx`:
- Around line 7-52: The reviews array in Testimonials.tsx is hard-coded mock
data (reviews) being rendered as real testimonials; update the component to
avoid presenting placeholders as real feedback by either gating the mock data
behind an explicit flag/prop (e.g., NEXT_PUBLIC_SHOW_MOCK_REVIEWS or a
showMockReviews prop checked inside the Testimonials component) so it only
renders in dev/demo environments, or change the UI to render a clear
label/disclaimer such as "Example testimonials — not real customers" whenever
the reviews array is used; locate and modify the reviews array and the
Testimonials component render logic to implement the gating or add the visible
disclaimer.
In `@apps/web/components/landing/VideoGuides.tsx`:
- Around line 142-148: The iframe in the VideoGuides component currently lacks a
sandbox attribute; update the JSX iframe (the element using props videoSrc and
title in VideoGuides.tsx) to include a restrictive sandbox value that still
permits playback and embeds (for example include tokens like allow-same-origin,
allow-scripts, allow-presentation and allow-popups as needed) so external video
content is isolated while keeping the existing allow and allowFullScreen
attributes intact; adjust the sandbox tokens minimally to support the target
providers (e.g., YouTube) and test playback.
- Around line 109-153: The modal currently attaches the Escape key handler to
the non-focusable backdrop (the motion.div rendering when isOpen && hasVideo) so
keyboard events won't reliably fire; update the modal to be focusable (add
tabIndex={-1}) and use a ref (e.g., modalRef) to call modalRef.current.focus()
when isOpen becomes true, implement a focus trap around the inner motion.div (or
replace with a dialog component from `@radix-ui/react-dialog`) to keep tab focus
inside the modal, ensure the Escape handler uses that focusable element or a
keydown listener attached via the ref to call setIsOpen(false), and capture the
trigger element (e.g., triggerRef) to restore focus to
triggerRef.current.focus() when the modal closes.
In `@apps/web/components/landing/WhyLocal.tsx`:
- Around line 206-208: The paragraph in the WhyLocal component currently asserts
an absolute "no cloud" stance; update the copy in WhyLocal.tsx (the three
similar text blocks around the component) to remove absolutes and instead
communicate a local-first, optional cloud-sync model—e.g., rephrase to
"Local-first: your notes are stored on your device by default, with optional
cloud sync when you enable it"—and make the same change for the two other
occurrences (the similar text blocks referenced in the review) so all messaging
consistently reflects optional sync.
In `@apps/web/components/magicui/animated-beam.tsx`:
- Around line 28-46: The component's default prop duration uses Math.random()
which can differ between server and client and cause hydration mismatches;
change the implementation so duration is initialized once on the client using a
stable hook (e.g., useRef or useState with lazy initializer) inside the
AnimatedBeam component instead of as a top-level default parameter, keeping the
same fallback range (Math.random() * 3 + 4) and ensuring any references still
use the stable ref/state (look for the duration parameter in the AnimatedBeam
function signature and where it's consumed).
In `@apps/web/components/magicui/animated-grid-pattern.tsx`:
- Around line 100-139: Detect users' reduced-motion preference and short-circuit
the animated layer: import useReducedMotion from 'framer-motion' (or read
matchMedia('(prefers-reduced-motion: reduce)') ), call const
prefersReducedMotion = useReducedMotion(); then in the render where you map
squares (the motion.rect usage inside the squares.map), if prefersReducedMotion
render plain SVG <rect> elements (use the same props: key={id}, width={width-1},
height={height-1}, x={x*width+1}, y={y*height+1}, fill="currentColor",
strokeWidth="0", and set opacity={maxOpacity}) and do not attach transition or
onAnimationComplete; otherwise keep the existing motion.rect with transitions
and onAnimationComplete(updateSquarePosition). This ensures updateSquarePosition
and repeated animations are skipped for reduced-motion users.
In `@apps/web/components/magicui/animated-shiny-text.tsx`:
- Around line 8-20: The component AnimatedShinyText currently sets a style
object with the CSS variable '--shiny-width' but then spreads ...props after it,
allowing a consumer-supplied style to overwrite the variable; modify the render
to merge the component style with props.style (e.g., create a mergedStyle = {
...(props.style || {}), '--shiny-width': `${shimmerWidth}px` } or vice-versa
depending on desired precedence) and pass mergedStyle as the style prop so the
CSS variable is preserved while still honoring caller styles; update references
around the span render where style and ...props are applied to ensure
'--shiny-width' is retained.
- Around line 21-29: The shimmer currently always animates in the
AnimatedShinyText component (className passed to cn), so add motion-reduce
fallbacks to respect prefers-reduced-motion: update the shine-related classes
(the 'animate-shiny-text [background-size:...] bg-clip-text
[background-position:...] bg-no-repeat [transition:...]' and the gradient class)
to include motion-reduce utilities such as 'motion-reduce:animate-none' and a
static background-position via 'motion-reduce:[background-position:0_0]' (or a
preferred static position) so users with prefers-reduced-motion no longer see
continuous motion.
In `@apps/web/components/magicui/dot-pattern.tsx`:
- Around line 53-70: The dot animation timings are being re-generated on every
resize because delay and duration are computed inside the dots array mapping;
fix this by memoizing the computed dots (positions + random timing) so they only
change when their true inputs change (e.g., dimensions, safeWidth, safeHeight,
cx, cy, x, y) or by deriving delay/duration deterministically from
index/position (seeded) instead of Math.random() each render; update the code
that builds dots (the const dots = Array.from(...) mapping) to use useMemo or a
deterministic seeding function so delay and duration remain stable across
resizes.
- Around line 37-48: Replace the window resize listener in the useEffect with a
ResizeObserver so container-aware resizing triggers updateDimensions;
specifically, create a ResizeObserver that observes containerRef.current and
calls the existing updateDimensions function (which reads
containerRef.current.getBoundingClientRect() and calls setDimensions), start
observing on mount and disconnect the observer in the cleanup; mirror the
approach used in AnimatedBeam to ensure layout changes (not just window resizes)
update the dot pattern.
In `@apps/web/components/magicui/hero-video-dialog.tsx`:
- Around line 109-151: The modal currently opens with isVideoOpen but does not
trap keyboard focus; implement a focus trap so Tab/Shift+Tab cannot leave the
dialog and restore focus on close: add a ref for the close button (e.g.,
closeBtnRef) and a ref to capture the previously focused element when
isVideoOpen becomes true (save document.activeElement), programmatically focus
closeBtnRef on open, and add a keydown handler on the dialog container (the
motion.div with role="dialog") that intercepts Tab and Shift+Tab to cycle
through focusable elements inside the modal (or replace the modal container with
a focus-trap-react wrapper if you prefer a library). Also ensure you restore
focus to the saved active element when setIsVideoOpen(false) runs and keep
aria-modal/role attributes as-is.
- Around line 88-94: Replace the native <img> with Next.js Image to enable
optimization: import Image from "next/image" and use the Image component in
place of the <img> tag that currently uses thumbnailSrc, thumbnailAlt, width,
height and className (in this file's HeroVideoDialog component). Keep the
numeric width and height props (or switch to fill with parent styling if
desired), pass src={thumbnailSrc} and alt={thumbnailAlt}, retain className for
styling, and remove attributes unsupported by next/image; ensure thumbnailSrc is
a valid string/URL or add it to next.config.js domains if external.
- Around line 117-121: The Escape handler on the non-focusable div won’t fire;
replace or augment the inline onKeyDown with a global key listener: inside the
HeroVideoDialog component add a useEffect that registers
document.addEventListener('keydown', handler) where handler checks event.key ===
'Escape' and calls setIsVideoOpen(false), and remove the listener in the
cleanup; alternatively make the dialog container focusable by adding
tabIndex={0} and auto-focusing it via a ref/useEffect so the existing onKeyDown
on the div will work. Ensure you reference the existing onKeyDown and
setIsVideoOpen symbols when making the change and include proper cleanup for the
event listener.
In `@apps/web/components/magicui/marquee.tsx`:
- Around line 47-74: The marquee currently forces continuous animation (classes
like 'animate-marquee' / 'animate-marquee-vertical') and shows repeated copies
even when users prefer reduced motion; update the container and inner-copy
rendering to respect prefers-reduced-motion by: 1) adding Tailwind motion-reduce
utilities to disable animation on the inner strip (e.g., replace or augment
'animate-marquee'/'animate-marquee-vertical' with 'motion-reduce:animate-none'
so animations stop when motion is reduced), 2) hide extra copies under
motion-reduce (when rendering the repeated copies from safeRepeat, add a
conditional to apply 'motion-reduce:hidden' or skip rendering replicas when
motion is reduced so only the first child remains), and 3) allow manual
scrolling for the remaining row by enabling overflow auto in motion-reduce on
the outer container (e.g., add 'motion-reduce:overflow-auto' / keep
'overflow-hidden' otherwise). Update logic tied to safeRepeat, the outer div
props/className block, and the inner div className that contains children
(references: safeRepeat, vertical, pauseOnHover, reverse, and the inner div
using 'animate-marquee'/'animate-marquee-vertical') to implement these changes.
In `@apps/web/components/Navbar.tsx`:
- Around line 117-169: The Docs dropdown is only toggled via CSS group
hover/focus and lacks keyboard activation and ARIA state; update the Navbar
component to manage an explicit open state (e.g., isDocsOpen) for the Docs
trigger button, add an onClick handler on that button to toggle isDocsOpen, set
aria-haspopup="true" and aria-expanded={isDocsOpen}, and conditionally apply the
visibility classes on the dropdown container (the div currently using
group-hover/group-focus-within) based on isDocsOpen; also ensure keyboard
support by closing on Escape and moving focus into the menu when opened (use
refs and a blur/keydown handler around the button/menu rendering that maps to
the docsItems rendering logic).
In `@apps/web/content/docs/architecture/overview.mdx`:
- Around line 25-27: Remove the stale Folder node describing "docs-site"
(VitePress) from the monorepo tree in overview.mdx: delete the <Folder
name="docs-site"> and its child <File name="Documentation (VitePress)" />
entries so the tree no longer references the deprecated VitePress site; if
desired, replace it with a note or entry that reflects the current apps/web
structure (Next.js + Fumadocs under apps/web/** with marketing in
app/(marketing)/ and docs in content/docs/) to avoid misdirecting contributors.
In `@docs/plans/2026-03-12-website-redesign-design.md`:
- Line 120: Add a short governance note in this plan beneath the "magicui/"
entry stating the upstream repository URL, the exact commit hash or tag and date
it was copied, the license(s) and SPDX identifier, any attribution/NOTICE
requirements and required license text, the internal owner/maintainer
responsible for tracking updates, and the update/review cadence and process for
pulling upstream fixes; record this metadata both inline in this document (near
the "magicui/" line) and in the central license provenance file (e.g.,
LICENSES.md) so compliance and update risk are tracked.
- Line 11: The line "**Theme: Dark mode only**" creates an accessibility risk;
update the plan to remove the hard constraint and instead state a default plus
fallback and policy: change that line to something like "Theme: Dark mode by
default with a supported light-theme fallback" and add a short subsection that
(1) mandates providing a light-theme that meets WCAG contrast requirements, (2)
honors user/system color-scheme preferences, and (3) documents any justified
exceptions (with owner and accessibility review) so readers know when dark-only
is acceptable and how to request exceptions.
- Around line 109-113: Replace the ambiguous "shadcn/ui" entry with a clear
split: move shadcn/ui into a tooling/scaffolding note describing it as a
CLI/code-generator (scaffold only), and separately enumerate the actual
runtime/build dependencies that the scaffold injects (e.g., the concrete
component packages and build-time deps such as tailwindcss, framer-motion, font
packages, tailwindcss-animate). Update the bullet list so "shadcn/ui" is
described as a developer tool/CLI and add a short parenthetical listing of the
true runtime/build packages that must be installed and maintained alongside
functions/classes generated by the shadcn CLI.
In `@docs/plans/api-reference.md`:
- Around line 409-415: The documented route GET /newsletter/status/:email
exposes email addresses in URLs; change the API and docs to avoid path
parameters for emails by replacing the route GET /newsletter/status/:email with
a privacy-safe alternative such as GET /newsletter/status?email=... or POST
/newsletter/status with { "email": "..." } in the request body, update the
documentation block for the route (title, request example, response, and errors)
to show the new parameter location and example request, and ensure you call out
the 400 invalid email error still applies and mention privacy rationale in the
docs; reference the route identifier GET /newsletter/status/:email to locate and
update the implementation and docs.
In `@packages/ai-assistant/src/aiCommandTypes.test.ts`:
- Around line 202-215: Add a test that covers the case of valid JSON with the
wrong shape for AiCommandPreset: create a JSON string like JSON.stringify({ foo:
'bar' }) and call parsePreset on it, then assert the expected behavior (either
that parsePreset returns a value that does not match AiCommandPreset and does
not throw, or that it throws a validation error) to document the intended
contract; reference parsePreset, serializePreset and AiCommandPreset in the test
and add a brief comment specifying the expected result so future reviewers know
whether callers must validate the returned structure.
In `@packages/ai-assistant/src/aiCommandTypes.ts`:
- Around line 173-212: validateAiCommandPreset currently only enforces name,
version, and commands while allowing optional fields like author and description
to be any type; update validateAiCommandPreset to validate optional fields by
checking if preset.author and preset.description are either undefined/null or
strings, and if present ensure they are strings (and optionally non-empty after
trim) otherwise push errors (e.g., field: 'author' / 'description', message:
'author must be a string' / 'description must be a string'); reference the
validateAiCommandPreset function and the existing errors array to add these
checks before returning errors so malformed optional fields are rejected early.
- Around line 131-160: validateAiCommandDefinition currently doesn't reject
unknown template tokens so typos like {{titel}} slip through; update
validateAiCommandDefinition to parse all {{...}} placeholders from
userPromptTemplate (e.g. with a simple regex) and for each placeholder verify it
exists in AI_TEMPLATE_PLACEHOLDERS, pushing an error for any unknown placeholder
(field: 'userPromptTemplate', message: 'contains unknown placeholder:
{{name}}'); this mirrors how resolveTemplate() works but fails fast during
validation to prevent raw tokens being sent to the model.
In `@packages/api/src/middleware/auth.ts`:
- Around line 78-120: The current catch block around the middleware is too broad
and converts any error (including route 5xxs) into a 401; narrow the try/catch
to only wrap the JWT verification logic (the code that calls
jose.verify/decoding and related jose.errors.* checks) and move await next()
outside that try so route errors propagate normally; in the catch, only handle
jose.errors.JWTExpired, JWTClaimValidationFailed, JWTInvalid, JWSInvalid,
JWSSignatureVerificationFailed and rethrow any non-JOSE/non-HTTPException errors
(preserve HTTPException by rethrowing it) so unrelated failures are not
rewritten to Authentication failed in the HTTPException constructors you already
use.
In `@packages/command-registry/src/definitions/ai.ts`:
- Around line 29-36: The command definition with id 'ai:tweet' uses the
'Twitter' icon and the name 'Convert to Tweet' which references the old platform
name; update the object (id 'ai:tweet', name 'Convert to Tweet', icon:
'Twitter', showInPalette) to use either the current platform naming (e.g.,
rename to 'Convert to X' and swap the icon to an 'X' variant) or a neutral
icon/name (e.g., 'Convert to Post' and a generic 'Post' or 'Share' icon from
lucide-react) so branding is accurate and future-proof; adjust only the name and
icon fields accordingly and ensure any imports/usages referencing the 'Twitter'
icon are updated to the chosen replacement.
---
Outside diff comments:
In `@apps/desktop/src/main/services/syncService.ts`:
- Around line 517-627: The pull() failure path and the push() error branch
currently return or log without updating the sync error bookkeeping, so update
both places to set this.state.lastError (use pullResult.error or
pushResult.error), increment this.state.consecutiveFailures, call
this.noteRepository.completeSyncHistoryEntry with an 'error' status if not
already done, and emitStatus with type 'sync-error' (or 'auth-expired' if the
error indicates auth) and an appropriate error field, then return early instead
of continuing to the success flow; do this for the pull failure block where
pullResult.success is false and the push failure branch where pushResult.success
is false, and ensure you do not call resetAutoSyncInterval() or clear
lastError/consecutiveFailures in these error paths so the error/backoff
bookkeeping remains correct.
In `@apps/desktop/src/renderer/components/ai/AiPanel.tsx`:
- Around line 245-247: The list is using the array index as the React key which
can break reconciliation; change the message model to include a stable unique id
(e.g., add an id field when messages are created in the code paths that push
messages such as sendMessage/addMessage) and use that id as the key in the
render (replace key={i} in messages.map(... <AiMessage ... />) with
key={msg.id}); generate ids with a reliable generator (nanoid/uuid or a
timestamp+counter) and update any types/interfaces that describe messages to
include the id field.
In `@apps/web/app/`(marketing)/faq/page.tsx:
- Around line 54-61: Update the FAQ entries in the FAQ array inside page.tsx so
they no longer contradict the 0.9.0 release: change the "Can I use plugins?"
answer to acknowledge plugin support (mention Plugin API, theme system, plugin
inspector and CLI plugin commands) and change the "Does Readied have AI
features?" answer to acknowledge AI capabilities (mention AI Commands/Cmd+K
v1/v2, RAG support and AI knowledge features), and keep phrasing concise and
user-facing; locate the entries by matching the question strings "Can I use
plugins?" and "Does Readied have AI features?" and replace their answer values
accordingly.
In `@packages/ai-assistant/src/rag.ts`:
- Around line 41-60: The messages array currently includes the full history
regardless of mode, so when mode === 'ask-notes' filter out assistant turns
before building messages: replace usage of history in the messages construction
with a filteredHistory = history.filter(m => !(mode === 'ask-notes' && m.role
=== 'assistant')) (or equivalent) and then set messages: ClaudeMessage[] =
[...filteredHistory, { role: 'user', content: query }]; ensure you reference the
existing variables input.mode (or mode), history, and messages/ClaudeMessage to
locate where to change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9ab57ab1-4b2d-44a7-9e67-23a48514a1f3
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (128)
.gitignoreCHANGELOG.mdapps/desktop/package.jsonapps/desktop/src/main/index.tsapps/desktop/src/main/services/apiClient.tsapps/desktop/src/main/services/syncService.tsapps/desktop/src/preload/index.tsapps/desktop/src/renderer/App.tsxapps/desktop/src/renderer/components/ai/AiPanel.tsxapps/desktop/src/renderer/components/sidebar/Sidebar.tsxapps/desktop/src/renderer/components/sidebar/SidebarFooter.tsxapps/desktop/src/renderer/components/sync/LoginModal.module.cssapps/desktop/src/renderer/components/sync/LoginModal.tsxapps/desktop/src/renderer/components/sync/index.tsapps/desktop/src/renderer/hooks/useRegisterAiCommands.tsapps/desktop/src/renderer/hooks/useSyncOnboarding.tsapps/desktop/src/renderer/pages/settings/SettingsApp.tsxapps/desktop/src/renderer/pages/settings/components/SettingsSidebar.tsxapps/desktop/src/renderer/pages/settings/sections/AiSection.tsxapps/desktop/src/renderer/plugins/aiAssistant.tsxapps/desktop/src/renderer/stores/settings/schema.tsapps/desktop/src/renderer/stores/settings/settingsStore.tsapps/desktop/src/renderer/stores/syncStore.tsapps/desktop/src/renderer/styles/ai-panel.cssapps/desktop/src/renderer/styles/global.cssapps/web/._mdx-components.tsxapps/web/._package.jsonapps/web/app/(marketing)/._page.tsxapps/web/app/(marketing)/auth/verify/._AuthVerifyContent.tsxapps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsxapps/web/app/(marketing)/changelog/._page.tsxapps/web/app/(marketing)/changelog/page.tsxapps/web/app/(marketing)/download/._page.tsxapps/web/app/(marketing)/download/page.tsxapps/web/app/(marketing)/faq/._page.tsxapps/web/app/(marketing)/faq/page.tsxapps/web/app/(marketing)/page.tsxapps/web/app/(marketing)/philosophy/._page.tsxapps/web/app/(marketing)/philosophy/page.tsxapps/web/app/(marketing)/plugins/._page.tsxapps/web/app/(marketing)/plugins/page.tsxapps/web/app/(marketing)/pricing/._page.tsxapps/web/app/(marketing)/pricing/page.tsxapps/web/app/(marketing)/privacy/._page.tsxapps/web/app/(marketing)/privacy/page.tsxapps/web/app/(marketing)/terms/._page.tsxapps/web/app/(marketing)/terms/page.tsxapps/web/app/._globals.cssapps/web/app/._layout.tsxapps/web/app/docs/._layout.tsxapps/web/app/docs/[[...slug]]/._page.tsxapps/web/app/docs/[[...slug]]/page.tsxapps/web/app/docs/layout.tsxapps/web/app/globals.cssapps/web/app/layout.tsxapps/web/components/._FaqAccordion.tsxapps/web/components/._Footer.tsxapps/web/components/._NavDropdown.tsxapps/web/components/._Navbar.tsxapps/web/components/FaqAccordion.tsxapps/web/components/Footer.tsxapps/web/components/MobileNav.tsxapps/web/components/Navbar.tsxapps/web/components/landing/._Audience.tsxapps/web/components/landing/._Features.tsxapps/web/components/landing/._Hero.tsxapps/web/components/landing/._SocialProof.tsxapps/web/components/landing/._WhyLocal.tsxapps/web/components/landing/Audience.tsxapps/web/components/landing/ComparisonTable.tsxapps/web/components/landing/CreatorStory.tsxapps/web/components/landing/Features.tsxapps/web/components/landing/Hero.tsxapps/web/components/landing/SocialProof.tsxapps/web/components/landing/Testimonials.tsxapps/web/components/landing/VideoGuides.tsxapps/web/components/landing/WhyLocal.tsxapps/web/components/magicui/animated-beam.tsxapps/web/components/magicui/animated-grid-pattern.tsxapps/web/components/magicui/animated-shiny-text.tsxapps/web/components/magicui/border-beam.tsxapps/web/components/magicui/dot-pattern.tsxapps/web/components/magicui/hero-video-dialog.tsxapps/web/components/magicui/marquee.tsxapps/web/components/magicui/number-ticker.tsxapps/web/components/magicui/shimmer-button.tsxapps/web/components/magicui/text-reveal.tsxapps/web/components/ui/accordion.tsxapps/web/components/ui/badge.tsxapps/web/components/ui/button.tsxapps/web/components/ui/card.tsxapps/web/components/ui/separator.tsxapps/web/components/ui/sheet.tsxapps/web/content/docs/._index.mdxapps/web/content/docs/architecture/._overview.mdxapps/web/content/docs/architecture/overview.mdxapps/web/content/docs/guide/._principles.mdxapps/web/content/docs/guide/principles.mdxapps/web/content/docs/index.mdxapps/web/content/docs/plugins/._getting-started.mdxapps/web/content/docs/plugins/getting-started.mdxapps/web/lib/layout.shared.tsxapps/web/lib/utils.tsapps/web/mdx-components.tsxapps/web/package.jsondocs/plans/2026-03-12-roadmap-auth-sync-ai.mddocs/plans/2026-03-12-website-redesign-design.mddocs/plans/2026-03-12-website-redesign-implementation.mddocs/plans/api-reference.mdpackage.jsonpackages/ai-assistant/package.jsonpackages/ai-assistant/src/aiCommandTypes.test.tspackages/ai-assistant/src/aiCommandTypes.tspackages/ai-assistant/src/index.tspackages/ai-assistant/src/prompts.tspackages/ai-assistant/src/rag.tspackages/api/src/index.tspackages/api/src/middleware/auth.tspackages/command-registry/src/definitions/ai.tspackages/command-registry/src/definitions/editor.tspackages/command-registry/src/definitions/index.tspackages/command-registry/src/types.tspackages/licensing/package.jsonpackages/plugin-api/src/ai/aiCommandStore.tspackages/plugin-api/src/index.tspackages/plugin-api/src/lifecycle/PluginRegistry.tspackages/plugin-api/src/types.tspackages/product-config/src/index.ts
💤 Files with no reviewable changes (2)
- apps/web/components/landing/ComparisonTable.tsx
- apps/web/components/MobileNav.tsx
| if (response.status === 401 && tokens) { | ||
| const refreshed = await this.refreshAccessToken(); | ||
| if (refreshed) { | ||
| // Retry request with new token | ||
| return this.request<T>(endpoint, options, 0); | ||
| } else { | ||
| // Refresh failed - clear tokens | ||
| await this.tokenStorage.clearTokens(); | ||
| throw new ApiError(401, 'Session expired. Please sign in again.'); | ||
| const refreshResult = await this.refreshAccessToken(); | ||
| switch (refreshResult.type) { | ||
| case 'success': | ||
| return this.request<T>(endpoint, options, 0); |
There was a problem hiding this comment.
Guard the post-refresh retry to a single auth retry.
On success this recursively calls request() again, but the 401 path never records that a refresh was already attempted. If the retried call still gets a 401, this can keep refreshing and recursing until the stack or the backend gives out.
Suggested fix
- private async request<T>(endpoint: string, options: RequestInit = {}, retries = 3): Promise<T> {
+ private async request<T>(
+ endpoint: string,
+ options: RequestInit = {},
+ retries = 3,
+ hasRetriedAuth = false
+ ): Promise<T> {
@@
- if (response.status === 401 && tokens) {
+ if (response.status === 401 && tokens && !hasRetriedAuth) {
const refreshResult = await this.refreshAccessToken();
switch (refreshResult.type) {
case 'success':
- return this.request<T>(endpoint, options, 0);
+ return this.request<T>(endpoint, options, 0, true);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/main/services/apiClient.ts` around lines 215 - 219, The 401
handling in request() calls refreshAccessToken() and on success recursively
calls request() without recording that a refresh was already attempted, allowing
infinite refresh/recursion; modify request(endpoint, options, retryCount = 0) to
use the existing third parameter as a retry counter, only attempt refresh and
retry when response.status === 401 and retryCount === 0, and when retrying call
this.request<T>(endpoint, options, retryCount + 1); if retryCount > 0 and a 401
occurs, surface the auth error (don't refresh again).
| default: | ||
| await this.tokenStorage.clearTokens(); | ||
| throw new ApiError( | ||
| 401, | ||
| refreshResult.message ?? 'Authentication failed. Please sign in again.' | ||
| ); |
There was a problem hiding this comment.
Treat transient refresh statuses as retryable, not logout-worthy.
Anything outside 401/403/5xx currently becomes unknown, and the default branch clears stored tokens. That means a throttled or timed-out refresh like 408/429 will sign the user out even though retrying later is the correct recovery path.
Also applies to: 340-352
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/main/services/apiClient.ts` around lines 235 - 240, The
default branch currently calls tokenStorage.clearTokens() and treats any
non-401/403/5xx refreshResult as logout-worthy; instead, only clear tokens and
throw ApiError for explicit unauthorized statuses (401/403) or definitive
permanent failures, and for transient statuses (e.g., 408, 429, network timeouts
in refreshResult.status) treat them as retryable: do NOT call
tokenStorage.clearTokens(), return/throw an ApiError or a specific transient
error that preserves tokens and signals retry, and include refreshResult.message
in the ApiError; apply the same change to the other refresh-handling block that
references tokenStorage.clearTokens() around the 340-352 region so transient
refresh statuses no longer log the user out.
| /** | ||
| * Check if an error represents a 401 Unauthorized response. | ||
| */ | ||
| function isAuthError(error: unknown): boolean { | ||
| if (!(error instanceof Error)) return false; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('401') || msg.includes('unauthorized'); | ||
| } |
There was a problem hiding this comment.
Use ApiError.statusCode for auth-expiry detection.
ApiClient now throws ApiError(401, 'Session expired. Please sign in again.') and other 401s whose message may not contain "401" or "unauthorized". This helper will miss those cases, so auto-sync backs off instead of emitting auth-expired and stopping.
Suggested fix
-import type { ApiClient, SyncChange, NotebookSyncChange, NotebookPushResult } from './apiClient.js';
+import { ApiError } from './apiClient.js';
+import type { ApiClient, SyncChange, NotebookSyncChange, NotebookPushResult } from './apiClient.js';
@@
function isAuthError(error: unknown): boolean {
- if (!(error instanceof Error)) return false;
+ if (error instanceof ApiError) return error.statusCode === 401;
+ if (!(error instanceof Error)) return false;
const msg = error.message.toLowerCase();
return msg.includes('401') || msg.includes('unauthorized');
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/main/services/syncService.ts` around lines 84 - 91, The
isAuthError helper currently inspects only error.message text and will miss
ApiError instances that carry a 401 in their statusCode; update
isAuthError(error: unknown) to detect ApiError by checking for an object with a
numeric statusCode === 401 (in addition to the existing message checks) so that
ApiError(401, ...) triggers auth-expired; reference the isAuthError function and
ensure the check runs before the message-based checks to safely handle
non-Error/ApiError values.
| /** | ||
| * Throw if the current sync operation has been aborted. | ||
| */ | ||
| private checkAborted(): void { | ||
| if (this.abortController?.signal.aborted) { | ||
| throw new Error('Sync aborted'); | ||
| } | ||
| } |
There was a problem hiding this comment.
The abort-on-logout path is still best-effort and reports a false sync error.
abortController is only polled via checkAborted(). Because none of the apiClient.* calls in this sync cycle take a signal, the current fetch keeps running until the next phase boundary; once checkAborted() does fire, the catch path increments failure counters and emits sync-error, so a user-initiated logout looks like a broken sync. Propagate AbortSignal into ApiClient.request() and treat aborts as cancellation instead of failure.
Also applies to: 481-588, 650-682
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/main/services/syncService.ts` around lines 457 - 464,
checkAborted() currently polls abortController but API calls in the sync cycle
don't receive a signal, so a logout abort is misreported as a sync failure;
update ApiClient.request(...) to accept an optional AbortSignal and thread that
signal into every apiClient.* call made during the sync cycle (the calls invoked
around checkAborted() and the sync phases), then when an AbortError or
signal.aborted is observed treat it as a cancellation: rethrow or return a
distinct cancellation result and ensure the catch path does not increment the
failure counters or emit the 'sync-error' event for aborts (instead perform any
cleanup and exit gracefully). Ensure you reference and use the existing
abortController.signal and checkAborted() semantics so other logic still detects
aborts.
| const model = aiSettings.apiKey | ||
| ? aiSettings.model | ||
| : getConfig<string>('model') || 'claude-sonnet-4-20250514'; | ||
| const maxContextNotes = aiSettings.apiKey | ||
| ? aiSettings.maxContextNotes | ||
| : getConfig<number>('maxContextNotes') || 5; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Settings fallback logic may be confusing.
The condition aiSettings.apiKey is used to determine whether to read from the settings store or fall back to plugin config. This works but is indirect—if a user has an API key in the store but wants to use plugin config for model/limits, this logic wouldn't support that. Consider documenting this behavior or simplifying to always prefer settings store values when defined.
💡 Clearer fallback pattern
- const model = aiSettings.apiKey
- ? aiSettings.model
- : getConfig<string>('model') || 'claude-sonnet-4-20250514';
- const maxContextNotes = aiSettings.apiKey
- ? aiSettings.maxContextNotes
- : getConfig<number>('maxContextNotes') || 5;
+ const model = aiSettings.model || getConfig<string>('model') || 'claude-sonnet-4-20250514';
+ const maxContextNotes = aiSettings.maxContextNotes ?? getConfig<number>('maxContextNotes') ?? 5;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const model = aiSettings.apiKey | |
| ? aiSettings.model | |
| : getConfig<string>('model') || 'claude-sonnet-4-20250514'; | |
| const maxContextNotes = aiSettings.apiKey | |
| ? aiSettings.maxContextNotes | |
| : getConfig<number>('maxContextNotes') || 5; | |
| const model = aiSettings.model || getConfig<string>('model') || 'claude-sonnet-4-20250514'; | |
| const maxContextNotes = aiSettings.maxContextNotes ?? getConfig<number>('maxContextNotes') ?? 5; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/components/ai/AiPanel.tsx` around lines 64 - 69,
The current fallback uses aiSettings.apiKey as the gate for whether to prefer
aiSettings values vs getConfig, which prevents using stored API key while still
preferring plugin config for model/limits; update the logic in AiPanel.tsx so
each setting (model and maxContextNotes) individually prefers a defined value
from aiSettings if present, otherwise falls back to getConfig(...) and then a
hard default — change the expressions that set model and maxContextNotes
(currently checking aiSettings.apiKey) to check aiSettings.model and
aiSettings.maxContextNotes respectively (or use explicit null/undefined checks)
and retain the existing getConfig defaults.
| apps/web/ | ||
| ├── components/ | ||
| │ ├── ui/ ← shadcn/ui (Button, Card, Badge, etc.) | ||
| │ ├── magicui/ ← Magic UI (copied source, not npm) |
There was a problem hiding this comment.
Add source provenance + license tracking for copied Magic UI code.
Line 120 states Magic UI is copied source. That creates a compliance and update-risk gap unless the plan records upstream repo, commit/tag, and license obligations. Please add a short governance note in this doc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-03-12-website-redesign-design.md` at line 120, Add a short
governance note in this plan beneath the "magicui/" entry stating the upstream
repository URL, the exact commit hash or tag and date it was copied, the
license(s) and SPDX identifier, any attribution/NOTICE requirements and required
license text, the internal owner/maintainer responsible for tracking updates,
and the update/review cadence and process for pulling upstream fixes; record
this metadata both inline in this document (near the "magicui/" line) and in the
central license provenance file (e.g., LICENSES.md) so compliance and update
risk are tracked.
| ### `GET /newsletter/status/:email` | ||
|
|
||
| Check subscription status for an email. | ||
|
|
||
| **Response:** `{ subscribed: true, status: "subscribed", subscribedAt: "..." }` | ||
|
|
||
| **Errors:** `400` invalid email. |
There was a problem hiding this comment.
Consider privacy implications of email in URL path.
GET /newsletter/status/:email exposes the email address in the URL path, which can appear in server access logs, browser history, and referrer headers. Consider using a query parameter or POST request body instead for better privacy hygiene.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/api-reference.md` around lines 409 - 415, The documented route GET
/newsletter/status/:email exposes email addresses in URLs; change the API and
docs to avoid path parameters for emails by replacing the route GET
/newsletter/status/:email with a privacy-safe alternative such as GET
/newsletter/status?email=... or POST /newsletter/status with { "email": "..." }
in the request body, update the documentation block for the route (title,
request example, response, and errors) to show the new parameter location and
example request, and ensure you call out the 400 invalid email error still
applies and mention privacy rationale in the docs; reference the route
identifier GET /newsletter/status/:email to locate and update the implementation
and docs.
| if (typeof def.userPromptTemplate !== 'string' || def.userPromptTemplate.trim().length === 0) { | ||
| errors.push({ | ||
| field: 'userPromptTemplate', | ||
| message: 'userPromptTemplate is required and must be a non-empty string', | ||
| }); | ||
| } | ||
|
|
||
| if (def.icon !== undefined && typeof def.icon !== 'string') { | ||
| errors.push({ field: 'icon', message: 'icon must be a string if provided' }); | ||
| } | ||
|
|
||
| if ( | ||
| def.outputTarget !== undefined && | ||
| !['replace', 'insert', 'panel'].includes(def.outputTarget as string) | ||
| ) { | ||
| errors.push({ | ||
| field: 'outputTarget', | ||
| message: 'outputTarget must be "replace", "insert", or "panel"', | ||
| }); | ||
| } | ||
|
|
||
| if (def.description !== undefined && typeof def.description !== 'string') { | ||
| errors.push({ field: 'description', message: 'description must be a string if provided' }); | ||
| } | ||
|
|
||
| if (def.category !== undefined && typeof def.category !== 'string') { | ||
| errors.push({ field: 'category', message: 'category must be a string if provided' }); | ||
| } | ||
|
|
||
| return errors; |
There was a problem hiding this comment.
Reject unsupported {{placeholders}} during command validation.
resolveTemplate() leaves unknown placeholders untouched, but validateAiCommandDefinition() never checks userPromptTemplate against AI_TEMPLATE_PLACEHOLDERS. Presets with typos like {{titel}} will pass validation and then send raw template tokens to the model.
Suggested fix
if (typeof def.userPromptTemplate !== 'string' || def.userPromptTemplate.trim().length === 0) {
errors.push({
field: 'userPromptTemplate',
message: 'userPromptTemplate is required and must be a non-empty string',
});
+ } else {
+ for (const [, key] of def.userPromptTemplate.matchAll(PLACEHOLDER_REGEX)) {
+ if (!AI_TEMPLATE_PLACEHOLDERS.includes(key as AiTemplatePlaceholder)) {
+ errors.push({
+ field: 'userPromptTemplate',
+ message: `Unsupported placeholder "{{${key}}}"`,
+ });
+ }
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ai-assistant/src/aiCommandTypes.ts` around lines 131 - 160,
validateAiCommandDefinition currently doesn't reject unknown template tokens so
typos like {{titel}} slip through; update validateAiCommandDefinition to parse
all {{...}} placeholders from userPromptTemplate (e.g. with a simple regex) and
for each placeholder verify it exists in AI_TEMPLATE_PLACEHOLDERS, pushing an
error for any unknown placeholder (field: 'userPromptTemplate', message:
'contains unknown placeholder: {{name}}'); this mirrors how resolveTemplate()
works but fails fast during validation to prevent raw tokens being sent to the
model.
| if (typeof preset.name !== 'string' || preset.name.trim().length === 0) { | ||
| errors.push({ field: 'name', message: 'Preset name is required' }); | ||
| } | ||
|
|
||
| if (typeof preset.version !== 'string' || preset.version.trim().length === 0) { | ||
| errors.push({ field: 'version', message: 'Preset version is required' }); | ||
| } | ||
|
|
||
| if (!Array.isArray(preset.commands)) { | ||
| errors.push({ field: 'commands', message: 'commands must be an array' }); | ||
| return errors; | ||
| } | ||
|
|
||
| if (preset.commands.length === 0) { | ||
| errors.push({ field: 'commands', message: 'commands array must not be empty' }); | ||
| } | ||
|
|
||
| for (let i = 0; i < preset.commands.length; i++) { | ||
| const cmdErrors = validateAiCommandDefinition(preset.commands[i]); | ||
| for (const err of cmdErrors) { | ||
| errors.push({ | ||
| field: `commands[${i}].${err.field}`, | ||
| message: err.message, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Check for duplicate IDs | ||
| const ids = new Set<string>(); | ||
| for (const cmd of preset.commands) { | ||
| if (cmd && typeof cmd === 'object' && typeof (cmd as Record<string, unknown>).id === 'string') { | ||
| const id = (cmd as Record<string, unknown>).id as string; | ||
| if (ids.has(id)) { | ||
| errors.push({ field: 'commands', message: `Duplicate command id: "${id}"` }); | ||
| } | ||
| ids.add(id); | ||
| } | ||
| } | ||
|
|
||
| return errors; |
There was a problem hiding this comment.
Validate preset-level optional fields as well.
validateAiCommandPreset() enforces name, version, and commands, but author and description can currently be any type and still pass. That weakens the public import/export contract and pushes malformed data into downstream UI.
Suggested fix
if (typeof preset.version !== 'string' || preset.version.trim().length === 0) {
errors.push({ field: 'version', message: 'Preset version is required' });
}
+
+ if (preset.description !== undefined && typeof preset.description !== 'string') {
+ errors.push({ field: 'description', message: 'Preset description must be a string' });
+ }
+
+ if (preset.author !== undefined && typeof preset.author !== 'string') {
+ errors.push({ field: 'author', message: 'Preset author must be a string' });
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ai-assistant/src/aiCommandTypes.ts` around lines 173 - 212,
validateAiCommandPreset currently only enforces name, version, and commands
while allowing optional fields like author and description to be any type;
update validateAiCommandPreset to validate optional fields by checking if
preset.author and preset.description are either undefined/null or strings, and
if present ensure they are strings (and optionally non-empty after trim)
otherwise push errors (e.g., field: 'author' / 'description', message: 'author
must be a string' / 'description must be a string'); reference the
validateAiCommandPreset function and the existing errors array to add these
checks before returning errors so malformed optional fields are rejected early.
| // Re-throw HTTPException as-is to preserve specific error messages | ||
| if (error instanceof HTTPException) { | ||
| throw error; | ||
| } | ||
| if (error instanceof jose.errors.JWTExpired) { | ||
| throw new HTTPException(401, { message: 'Token expired' }); | ||
| throw new HTTPException(401, { | ||
| message: 'Token expired', | ||
| res: new Response(JSON.stringify({ error: 'Token expired' }), { | ||
| status: 401, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }), | ||
| }); | ||
| } | ||
| if (error instanceof jose.errors.JWTInvalid) { | ||
| throw new HTTPException(401, { message: 'Invalid token' }); | ||
| if (error instanceof jose.errors.JWTClaimValidationFailed) { | ||
| throw new HTTPException(401, { | ||
| message: 'Token validation failed', | ||
| res: new Response(JSON.stringify({ error: 'Token validation failed' }), { | ||
| status: 401, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }), | ||
| }); | ||
| } | ||
| throw new HTTPException(401, { message: 'Authentication failed' }); | ||
| if ( | ||
| error instanceof jose.errors.JWTInvalid || | ||
| error instanceof jose.errors.JWSInvalid || | ||
| error instanceof jose.errors.JWSSignatureVerificationFailed | ||
| ) { | ||
| throw new HTTPException(401, { | ||
| message: 'Invalid token', | ||
| res: new Response(JSON.stringify({ error: 'Invalid token' }), { | ||
| status: 401, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }), | ||
| }); | ||
| } | ||
| // Catch-all for any other errors (e.g. JOSEAlgNotAllowed, unexpected errors) | ||
| throw new HTTPException(401, { | ||
| message: 'Authentication failed', | ||
| res: new Response(JSON.stringify({ error: 'Authentication failed' }), { | ||
| status: 401, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Don't let auth handling rewrite unrelated route failures to 401.
This catch also wraps the await next() call in the same try, so any non-HTTPException thrown by a protected route gets converted into Authentication failed. That will mask real 5xxs as auth problems across the API. Narrow the try/catch to JWT verification and rethrow unexpected non-JOSE errors instead of mapping them to 401.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/middleware/auth.ts` around lines 78 - 120, The current catch
block around the middleware is too broad and converts any error (including route
5xxs) into a 401; narrow the try/catch to only wrap the JWT verification logic
(the code that calls jose.verify/decoding and related jose.errors.* checks) and
move await next() outside that try so route errors propagate normally; in the
catch, only handle jose.errors.JWTExpired, JWTClaimValidationFailed, JWTInvalid,
JWSInvalid, JWSSignatureVerificationFailed and rethrow any
non-JOSE/non-HTTPException errors (preserve HTTPException by rethrowing it) so
unrelated failures are not rewritten to Authentication failed in the
HTTPException constructors you already use.
| import { AiPanel } from './components/ai/AiPanel'; | ||
| import type { AiPanelMode } from '@readied/ai-assistant'; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Fix import ordering per linter rules.
The static analysis tool flags that the @readied/ai-assistant type import should occur before the ../preload/index type import.
♻️ Proposed fix
import { AiPanel } from './components/ai/AiPanel';
-import type { AiPanelMode } from '@readied/ai-assistant';
+import type { AiPanelMode } from '@readied/ai-assistant';
import { LicenseProvider } from './contexts/LicenseContext';Move the @readied/ai-assistant type import to group with other package type imports (before local type imports like ../preload/index).
🧰 Tools
🪛 GitHub Check: lint
[warning] 25-25:
@readied/ai-assistant type import should occur before type import of ../preload/index
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 24 - 25, Reorder the type
imports in App.tsx so external package types come before local types: move the
AiPanelMode type import from '@readied/ai-assistant' to the package/type imports
group (above the local preload type import like anything from
'../preload/index'), keeping the AiPanel component import where it belongs; this
satisfies the linter's import ordering rule while preserving the existing
symbols AiPanel and AiPanelMode.
| const aiConfigCache = useRef<Record<string, unknown>>({}); | ||
|
|
||
| // Load AI plugin config once on mount | ||
| useEffect(() => { | ||
| window.readied.pluginConfig.getAll('readied-ai-assistant').then(config => { | ||
| aiConfigCache.current = config ?? {}; | ||
| }); | ||
| }, []); |
There was a problem hiding this comment.
AI config cache is never refreshed after initial load.
The plugin config is loaded once on mount but aiConfigCache is never updated if the configuration changes during the session. If users modify AI settings, the panel will continue using stale values until the app restarts.
Consider either:
- Reloading config when the AI panel opens
- Subscribing to config changes
- Documenting that changes require restart
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 639 - 646, aiConfigCache is
only initialized on mount (useEffect calling window.readied.pluginConfig.getAll)
and never updated, so runtime config changes are ignored; update
aiConfigCache.current whenever the AI config changes by either subscribing to
plugin config changes (use the pluginConfig change/subscribe API, e.g.,
window.readied.pluginConfig.onChange / onDidChange / subscribe if available) and
updating aiConfigCache.current inside that handler with proper cleanup in the
effect, or reload the config each time the AI panel opens by calling
window.readied.pluginConfig.getAll again when the panel-open handler (or isOpen
state for the AI panel) runs; ensure you reference and update
aiConfigCache.current and remove the subscription on unmount to avoid leaks.
| import { EnableSyncModal } from '../sync'; | ||
| import { useSyncOnboarding } from '../../hooks/useSyncOnboarding'; |
There was a problem hiding this comment.
Reorder these imports to clear the lint warning.
Lines 22-23 are the exact imports the lint check is complaining about. Move the ../sync and ../../hooks/useSyncOnboarding imports above the local ./Sidebar* block so this file stays clean.
🧰 Tools
🪛 GitHub Check: lint
[warning] 23-23:
../../hooks/useSyncOnboarding import should occur before import of ./SidebarHeader
[warning] 22-22:
../sync import should occur before import of ./SidebarHeader
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/components/sidebar/Sidebar.tsx` around lines 22 -
23, The lint warning is caused by import order; move the external imports for
EnableSyncModal and useSyncOnboarding (symbols: EnableSyncModal,
useSyncOnboarding) above the local Sidebar-related imports (the ./Sidebar*
block) in Sidebar.tsx so third-party/parent imports appear before local/module
imports; reorder the two lines accordingly and keep existing relative paths and
named imports unchanged.
| const { data: counts } = useNoteCounts(); | ||
| const [dismissed, setDismissed] = useState(false); | ||
|
|
||
| const totalNotes = (counts as { active?: number })?.active ?? 0; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Type assertion could be made safer.
The inline type assertion (counts as { active?: number }) suggests the return type from useNoteCounts may be loosely typed. Consider typing the hook's return value more precisely or using a type guard.
♻️ Suggested improvement
- const { data: counts } = useNoteCounts();
-
- const totalNotes = (counts as { active?: number })?.active ?? 0;
+ const { data: counts } = useNoteCounts();
+
+ const totalNotes = counts && typeof counts === 'object' && 'active' in counts
+ ? (counts.active as number)
+ : 0;Or better, ensure useNoteCounts returns a properly typed result.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data: counts } = useNoteCounts(); | |
| const [dismissed, setDismissed] = useState(false); | |
| const totalNotes = (counts as { active?: number })?.active ?? 0; | |
| const { data: counts } = useNoteCounts(); | |
| const [dismissed, setDismissed] = useState(false); | |
| const totalNotes = counts && typeof counts === 'object' && 'active' in counts | |
| ? (counts.active as number) | |
| : 0; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/hooks/useSyncOnboarding.ts` around lines 21 - 24,
The inline assertion on counts is unsafe; update the useNoteCounts hook to
return a well-typed shape (e.g., { active?: number } or a Result type) or add a
narrow type guard before reading active so totalNotes is computed safely; locate
useNoteCounts and its call site in useSyncOnboarding.ts (where counts and
totalNotes are defined) and either adjust the hook's return type signature or
implement a small guard (e.g., check counts is non-null and has an active
number) and then compute totalNotes from that guarded value instead of using
(counts as { active?: number }).
| import { aiCommandStore } from '@readied/plugin-api'; | ||
| import type { AiCommandRegistration } from '@readied/plugin-api'; | ||
| import { validateAiCommandPreset, serializePreset } from '@readied/ai-assistant'; | ||
| import type { AiCommandPreset } from '@readied/ai-assistant'; | ||
| import styles from './Section.module.css'; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Fix import ordering per linter rules.
Static analysis flags that package imports (@readied/plugin-api, @readied/ai-assistant) should occur before local imports (../../../stores/settings).
♻️ Proposed fix
+import { aiCommandStore } from '@readied/plugin-api';
+import type { AiCommandRegistration } from '@readied/plugin-api';
+import { validateAiCommandPreset, serializePreset } from '@readied/ai-assistant';
+import type { AiCommandPreset } from '@readied/ai-assistant';
import { useState, useCallback, useSyncExternalStore } from 'react';
import { Eye, EyeOff, Zap, Loader2, CheckCircle, XCircle, Upload, Download } from 'lucide-react';
import { useSettingsStore, selectAi } from '../../../stores/settings';
import { SettingGroup } from '../components/SettingGroup';
import { SettingRow } from '../components/SettingRow';
import { Select, NumberInput } from '../components/controls';
-import { aiCommandStore } from '@readied/plugin-api';
-import type { AiCommandRegistration } from '@readied/plugin-api';
-import { validateAiCommandPreset, serializePreset } from '@readied/ai-assistant';
-import type { AiCommandPreset } from '@readied/ai-assistant';
import styles from './Section.module.css';🧰 Tools
🪛 GitHub Check: lint
[warning] 17-17:
@readied/ai-assistant type import should occur before import of ../../../stores/settings
[warning] 16-16:
@readied/ai-assistant import should occur before import of ../../../stores/settings
[warning] 15-15:
@readied/plugin-api type import should occur before import of ../../../stores/settings
[warning] 14-14:
@readied/plugin-api import should occur before import of ../../../stores/settings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx` around lines
14 - 18, Reorder the import statements so external package imports come first,
then local project imports, then style imports: move the package imports
(aiCommandStore, AiCommandRegistration from '@readied/plugin-api' and
validateAiCommandPreset, serializePreset, AiCommandPreset from
'@readied/ai-assistant') above the local import for '../../../stores/settings'
and keep the styles import (styles from './Section.module.css') last; ensure the
exact symbols aiCommandStore, AiCommandRegistration, validateAiCommandPreset,
serializePreset, AiCommandPreset, and styles are preserved and only their import
order is changed to satisfy the linter.
| <iframe | ||
| src={videoSrc} | ||
| title={title} | ||
| className="size-full" | ||
| allowFullScreen | ||
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | ||
| /> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding sandbox attribute for iframe security.
When embedding external videos (YouTube), adding a sandbox attribute restricts potentially dangerous capabilities while still allowing video playback.
🛡️ Proposed fix
<iframe
src={videoSrc}
title={title}
className="size-full"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+ sandbox="allow-scripts allow-same-origin allow-presentation"
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <iframe | |
| src={videoSrc} | |
| title={title} | |
| className="size-full" | |
| allowFullScreen | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | |
| /> | |
| <iframe | |
| src={videoSrc} | |
| title={title} | |
| className="size-full" | |
| allowFullScreen | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | |
| sandbox="allow-scripts allow-same-origin allow-presentation" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/components/landing/VideoGuides.tsx` around lines 142 - 148, The
iframe in the VideoGuides component currently lacks a sandbox attribute; update
the JSX iframe (the element using props videoSrc and title in VideoGuides.tsx)
to include a restrictive sandbox value that still permits playback and embeds
(for example include tokens like allow-same-origin, allow-scripts,
allow-presentation and allow-popups as needed) so external video content is
isolated while keeping the existing allow and allowFullScreen attributes intact;
adjust the sandbox tokens minimally to support the target providers (e.g.,
YouTube) and test playback.
| export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ | ||
| children, | ||
| className, | ||
| shimmerWidth = 100, | ||
| ...props | ||
| }) => { | ||
| return ( | ||
| <span | ||
| style={ | ||
| { | ||
| '--shiny-width': `${shimmerWidth}px`, | ||
| } as CSSProperties | ||
| } |
There was a problem hiding this comment.
Preserve --shiny-width when callers pass style.
Line 32 spreads ...props after the inline style, so a consumer-supplied style replaces the --shiny-width variable entirely and the shimmer stops working. Merge the incoming style object instead of letting it overwrite the component's CSS variable.
Proposed fix
export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
children,
className,
shimmerWidth = 100,
+ style,
...props
}) => {
return (
<span
style={
{
+ ...(style ?? {}),
'--shiny-width': `${shimmerWidth}px`,
} as CSSProperties
}Also applies to: 32-32
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/components/magicui/animated-shiny-text.tsx` around lines 8 - 20, The
component AnimatedShinyText currently sets a style object with the CSS variable
'--shiny-width' but then spreads ...props after it, allowing a consumer-supplied
style to overwrite the variable; modify the render to merge the component style
with props.style (e.g., create a mergedStyle = { ...(props.style || {}),
'--shiny-width': `${shimmerWidth}px` } or vice-versa depending on desired
precedence) and pass mergedStyle as the style prop so the CSS variable is
preserved while still honoring caller styles; update references around the span
render where style and ...props are applied to ensure '--shiny-width' is
retained.
| className={cn( | ||
| 'mx-auto max-w-md text-text-secondary', | ||
|
|
||
| // Shine effect | ||
| 'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]', | ||
|
|
||
| // Shine gradient | ||
| 'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80', | ||
|
|
There was a problem hiding this comment.
Honor reduced-motion preferences for this shimmer.
The effect is always animated here, so users with prefers-reduced-motion still get continuous motion on the landing page. Add a motion-reduce fallback so this degrades to a static style.
Proposed fix
- 'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
+ 'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite] motion-reduce:animate-none',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className={cn( | |
| 'mx-auto max-w-md text-text-secondary', | |
| // Shine effect | |
| 'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]', | |
| // Shine gradient | |
| 'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80', | |
| className={cn( | |
| 'mx-auto max-w-md text-text-secondary', | |
| // Shine effect | |
| 'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite] motion-reduce:animate-none', | |
| // Shine gradient | |
| 'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80', |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/components/magicui/animated-shiny-text.tsx` around lines 21 - 29,
The shimmer currently always animates in the AnimatedShinyText component
(className passed to cn), so add motion-reduce fallbacks to respect
prefers-reduced-motion: update the shine-related classes (the
'animate-shiny-text [background-size:...] bg-clip-text [background-position:...]
bg-no-repeat [transition:...]' and the gradient class) to include motion-reduce
utilities such as 'motion-reduce:animate-none' and a static background-position
via 'motion-reduce:[background-position:0_0]' (or a preferred static position)
so users with prefers-reduced-motion no longer see continuous motion.
| it('round-trips a preset through serialize/parse', () => { | ||
| const json = serializePreset(preset); | ||
| const parsed = parsePreset(json); | ||
| expect(parsed).toEqual(preset); | ||
| }); | ||
|
|
||
| it('produces valid JSON', () => { | ||
| const json = serializePreset(preset); | ||
| expect(() => JSON.parse(json)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('parsePreset throws on invalid JSON', () => { | ||
| expect(() => parsePreset('not json')).toThrow(); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding test for parsePreset with invalid structure.
The tests verify parsePreset throws on invalid JSON, but don't test what happens when JSON is valid but the structure doesn't match AiCommandPreset. If parsePreset should validate structure, consider adding:
it('parsePreset returns invalid structure as-is (caller must validate)', () => {
const invalidStructure = JSON.stringify({ foo: 'bar' });
// Document expected behavior: does it throw, return as-is, or validate?
expect(() => parsePreset(invalidStructure)).not.toThrow();
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ai-assistant/src/aiCommandTypes.test.ts` around lines 202 - 215, Add
a test that covers the case of valid JSON with the wrong shape for
AiCommandPreset: create a JSON string like JSON.stringify({ foo: 'bar' }) and
call parsePreset on it, then assert the expected behavior (either that
parsePreset returns a value that does not match AiCommandPreset and does not
throw, or that it throws a validation error) to document the intended contract;
reference parsePreset, serializePreset and AiCommandPreset in the test and add a
brief comment specifying the expected result so future reviewers know whether
callers must validate the returned structure.
| { | ||
| id: 'ai:tweet', | ||
| name: 'Convert to Tweet', | ||
| category: 'ai', | ||
| context: 'editor', | ||
| icon: 'Twitter', | ||
| showInPalette: true, | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor: Consider updating Twitter icon/naming.
The platform formerly known as Twitter is now "X". While Twitter icon from lucide-react is still recognizable, consider whether the branding aligns with current platform naming, or if a more generic "post" or "social" icon would be more future-proof.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/command-registry/src/definitions/ai.ts` around lines 29 - 36, The
command definition with id 'ai:tweet' uses the 'Twitter' icon and the name
'Convert to Tweet' which references the old platform name; update the object (id
'ai:tweet', name 'Convert to Tweet', icon: 'Twitter', showInPalette) to use
either the current platform naming (e.g., rename to 'Convert to X' and swap the
icon to an 'X' variant) or a neutral icon/name (e.g., 'Convert to Post' and a
generic 'Post' or 'Share' icon from lucide-react) so branding is accurate and
future-proof; adjust only the name and icon fields accordingly and ensure any
imports/usages referencing the 'Twitter' icon are updated to the chosen
replacement.
Configure Vercel to build apps/web from the monorepo root using pnpm workspaces. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Critical: - Guard apiClient post-refresh retry to prevent infinite 401 loop Major: - Treat transient refresh errors as retryable, not logout-worthy - Use ApiError.statusCode for auth-expiry detection in syncService - Handle intentional sync abort gracefully (no false error status) - Use product-config facade labels for pricing display - Fix FaqAccordion accessibility (ARIA roles, tab semantics) - Fix Testimonials semantic HTML (figcaption, star rating aria) - Add aria-labelledby to WhyLocal section - Fix animated-grid-pattern memory leak (mount guard, cleanup) - Fix hero-video-dialog keyboard handling (Escape via useEffect) Minor: - Guard LoginModal reset against rapid reopen race condition - Fix fallback color to match teal accent scheme - URL-encode auth verify token in deep links - Fix import order in docs layout - Suppress transitions in prefers-reduced-motion - Stabilize animated-beam random duration with useMemo - Add focus management to hero-video-dialog Nitpick: - Rename LoginModal → EnableSyncModal to match export name - Extract CSS color tokens for ai-panel and global styles - Improve Footer, dot-pattern, and docs page component quality Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary - Adds `auto-tag.yml` workflow that was created during v0.9.0 development but missed the squash merge to main - Without this workflow, release PRs don't trigger automatic tag creation, breaking the release pipeline ## Context The release pipeline chain is: `release/* PR merge → auto-tag creates vX.Y.Z → release.yml builds + publishes` This workflow was in `release/0.9.0` but the squash merge of PR #149 didn't include it, so v0.9.0 never got tagged or released. ## After merge Once this is on main, I'll manually create the `v0.9.0` tag to trigger the release build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Release 0.9.0 — a major milestone with AI features, sync stability, and a complete website redesign.
Version bumps
package.jsonapps/desktop/package.jsonChecklist
package.jsonapps/desktop/package.jsonv0.9.0after mergePost-merge
After merging to
main:v0.9.0on mainmainback intodeveloprelease/0.9.0branch🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation