Commit 2eb5064
authored
polish: image-provider errors, design retry, slides animation paths, address-feedback skill (BuilderIO#658)
* chore(agents-index): register address-feedback skill in AGENTS.md
Wires the new `.agents/skills/address-feedback/SKILL.md` into the skills table so the agent can find it.
* fix(design/editor): preserve pending generation for retry when generation stops without output
Two paths could clear a pending generation (`markGenerationStale` and the
generation-complete fallback timer). Only the stale path remembered the
prompt for retry. Now both share a `rememberPendingGenerationForRetry()`
helper, so a generation that the agent ends without writing files still
leaves the user a one-click retry banner with the original prompt /
model / engine / effort intact. Also adds dedicated 'Use generate-design,
do not call create-design' directives to the UI-started generation
prompt so the agent doesn't accidentally fork into questions or
variants on a UI-triggered run.
* fix(images/managed-provider): surface actionable error detail when Builder image API fails
Previously, when the Builder-managed image-generation route returned an
error (401, 402, 403, 429, 503...), users saw the generic
'BUILDER_PRIVATE_KEY not configured' fallback message even though the
real failure was unrelated (no credits, no space access, rate-limited,
transient outage).
The provider now:
- Captures the API error detail from the JSON body when present.
- Maps each known status to a specific user-facing message that
describes both the likely cause and the recovery path
(reconnect Builder space, switch to a space with credits, ask the
space admin to enable access, retry shortly, or add a Gemini API
key as the manual BYOK fallback).
- Routes 401/402/403 through `FeatureNotConfiguredError` so the
setup checklist points the user at the right credential, while
other statuses surface as a regular runtime error with the same
guidance text.
* feat(slides/animations): persistent element targeting via elementPath
Slide animations targeted children by a flat `elementIndex` into a
heuristically-chosen 'content container' (last child with ≥2
children). That broke as soon as the slide HTML was restructured
(intro added, wrapper inserted, list converted to grid, etc.) —
each existing animation silently slid onto the wrong element.
Each SlideAnimation now also stores `elementPath: number[]`, the
child-index path from the outer `.fmd-slide` wrapper, so animations
resolve to the same element across structural edits. `elementIndex`
stays for back-compat with existing decks.
Implementation:
- New `templates/slides/app/lib/slide-animation-elements.ts`
centralizes parsing + resolution: `parseSlideAnimationElements()`,
`resolveSlideAnimationElement()`, `findLegacyAnimationContainer()`,
`getSlideAnimationTargetKey()`, `getSlideAnimationTargetPreview()`.
- AnimationsPanel and PresentationView both consume the shared
module instead of duplicating the legacy 'last container'
heuristic.
- AnimationsPanel now keys 'used' / preview state by target key
(path + index) so the picker reflects the new model.
- Companion vitest spec covers parse / resolve / preview across
legacy index-only animations and new path-based ones.
* polish(clips/desktop/setup): longer permission labels + roomier permission button grid
- 'Screen' / 'Speech' → 'Screen Recording' / 'Speech Recognition' so
the macOS Privacy & Security pane the button opens matches what
it's labeled.
- Button grid now auto-fits 118px columns (instead of always two
columns) so labels don't truncate mid-word at narrow widths;
buttons also got a touch more vertical room (`min-height: 38px`,
`line-height: 1.15`, `white-space: normal`).
- Readiness summary row got matching breathing room: 32px min-height,
rounded corners, 6px/10px padding when collapsed (open state keeps
the existing square edge so it stitches to the body below).
* polish: drop dead images fallback branch, recognize styled-shape slide targets, add image-provider tests
Follow-ups to the earlier concurrent-agent commits on this branch.
- templates/images/server/lib/generation.ts: the new
`createBuilderImageGenerationFallbackError()` already covers every
`shouldFallback` case where the error is a
`BuilderImageGenerationError` (which is the only way we reach this
block when the response wasn't OK). The duplicate `if (shouldFallback)`
trailing branch that built a generic 'BUILDER_PRIVATE_KEY missing'
message was unreachable — drop it.
- templates/images/server/lib/generation.test.ts: vitest spec for the
error-mapping logic (401/402/403/429/503 message mapping, JSON
detail extraction, FeatureNotConfiguredError routing).
- templates/slides/app/lib/slide-animation-elements.ts: empty styled
shapes (e.g. a thin colored bar `<div style="width: 60px;
height: 4px; background: #00E5FF;">`) are real animation targets,
but `hasMeaningfulContent()` was filtering them out because they
have no text and no img/svg/etc. Add a `hasVisualStyle()` check
for inline style declarations that hint at a visible box
(background/border/box-shadow/width/height/min-width/min-height).
Spec covers a flex row containing a styled bar + a paragraph.
* fix(design/editor): add rememberPendingGenerationForRetry to markGenerationStale useCallback deps
The earlier commit on this branch (a2a29a1) introduced
`rememberPendingGenerationForRetry` and called it from both
`markGenerationStale` and `handleGenerationComplete`, but only
added it to the second callback's dependency array. Add it to
`markGenerationStale` too so an updated helper isn't captured stale.
* test(slides/animations): integration spec for AnimationsPanel element list + elementPath
Component-level spec for the AnimationsPanel + slide-animation-elements
integration introduced earlier on this branch. Covers:
- All animatable child elements appear in the panel's list (not just
the legacy 'last container with ≥2 children').
- Auto-fill populates each animation with both `elementIndex` (for
back-compat) and the new `elementPath` (for structural resilience),
including nested flex children.
* fix(slides): cleanup AnimationsPanel test DOM + gate slide↔URL sync on pending URL nav
- AnimationsPanel.test.tsx: add `cleanup()` in `afterEach` so the two
tests don't share rendered DOM between runs (vitest doesn't
auto-cleanup on each test by default for @testing-library/react).
- DeckEditor.tsx: introduce `pendingUrlSlideIdRef` to gate the
active-slide → URL sync effect. When an external nav (the agent
calling `navigate --slideIndex`) flips `activeSlideId`, the
URL→slide effect already wrote the new `slide` param; the
slide→URL effect would then race and rewrite it back to the
previous slide for one render. The ref records 'this slide change
came from a URL navigation' so the slide→URL effect skips its
write until the ref clears (or the targeted slide disappears).
* feat(clips): camera-composite lib — bubble overlay on top of display stream
New utility that takes a display `MediaStream` and a camera
`MediaStream` and produces a single composite `MediaStream` with
the camera framed as a corner bubble on top of the screen-share.
Configurable bubble size ratio + min/max pixel clamp, anchor
(`bottom-left` / `bottom-right`), margin ratio, and frame rate.
Returns a `cleanup()` handle so callers can release the canvas,
the requestAnimationFrame loop, and the underlying video elements
when the recording stops.
* fix(core/sync): per-key invalidation for app-state one-shot commands
Previously the server poll loop emitted a single `{ source: 'app-state',
key: '*' }` event whenever ANY application_state row changed, and the
client invalidated `navigate-command` / `show-questions` /
`__set_url__` on every such event. Noisy app-state keys like
`slide-fit-check` or per-tab UI state thus woke the navigation/
question readers on every poll cycle, breaking the optimistic
"close after read" pattern for those one-shot commands.
- Server (`packages/core/src/server/poll.ts`): fetch the changed rows,
emit one event per changed (key, owner) pair. Owner is propagated
so per-session targeting downstream still works.
- Client (`packages/core/src/client/use-db-sync.ts`):
`hasAppStateEvent(events, key)` checks whether the relevant batch
contained that specific key (or a legacy '*' wildcard for back-
compat), and only then invalidates the corresponding query.
* feat(clips): record composited camera bubble + calendar end-time validator helper
- templates/clips: screen+camera recordings now bake the camera
bubble into the recorded video stream so it appears in the saved
file. `recorder-engine.ts` calls the new
`createCameraCompositeStream()` (already shipped earlier on this
branch under `templates/clips/app/lib/camera-composite.ts`) when
mode is screen+camera, and threads the user's `cameraBubbleSize`
pre-record selection ("sm"/"md"/"lg") through to the
`bubbleSizeRatio` parameter. Adds a desktop-app copy of the same
lib at `templates/clips/desktop/src/lib/camera-composite.ts` so
the Electron recorder can use the same compositor without
cross-app imports.
- templates/calendar: `getEventEndValidationMessage()` returns the
right end-time validation message for the all-day, same-day, and
multi-day cases. Extracted so the form and other date-time
consumers can share the wording.
* chore: add changeset for @agent-native/core per-key sync
* test(core/poll-handler): update spec for per-key application_state emission
Companion test update for 3886279. The mock execute handler now
serves the new `SELECT session_id, key, updated_at FROM
application_state WHERE updated_at > ?` query shape, and the
assertion expects the per-key `{source: 'app-state', key:
'__screen_refresh__', owner: 'test@example.com'}` event instead of
the legacy `key: '*'` wildcard.
* fix(calendar/forms): use detailed end-time validation message in create + edit
CreateEventDialog and EventDetailPopover both replace the generic
'End must be after start' toast with `getEventEndValidationMessage()`
(added earlier on this branch), which picks the right phrasing for
the all-day, same-day-different-time, and multi-day cases.
* fix(calendar/sidebar): close sidebar overlay when the Google-accounts settings link is clicked
`GoogleAccountsSection` now accepts an `onClose` callback and
calls it from the per-account settings IconLink. Without this the
sidebar's mobile overlay stayed open over /settings after the
navigation completed, hiding the page the user just opened.
* feat(clips): thread cameraBubbleSize selection into web + desktop recorders
- templates/clips/app/routes/record.tsx now passes the user's
cameraSize selection ('sm' / 'md' / 'lg') into the
RecorderEngine, completing the wiring added in 7c5821e.
- templates/clips/desktop/src/lib/recorder.ts: for native (Tauri)
screen-camera recordings, the desktop recorder reads the saved
`bubble_size` setting via `invoke('load_bubble_size')` and feeds
it to `createCameraCompositeStream` so the saved video matches
the in-recorder bubble preview. The composite stream's cleanup is
registered with the recorder's `streamCleanups` queue so it stops
with the rest of the capture pipeline.
* fix(content/sidebar): keep deeply nested page rows reachable via horizontal scroll
When a document tree was nested many levels deep, the indent
(`depth * 16 + 12`) pushed the row text past the right edge of the
fixed-width sidebar and the row clipped — the user had no way to
see (let alone click) the inner pages.
- `DocumentTreeItem`: tighten the per-level indent from 16px to 12px
(matches the column tooltip target spacing) and give each row a
`min-w-56` so its label keeps its layout width regardless of
parent shrinkage.
- `DocumentSidebar`: wrap the tree in a `min-w-full w-max` container
so the inner list sizes to the widest row instead of clamping to
the sidebar's visible width.
- `scroll-area.tsx`: also render a horizontal `<ScrollBar>` so users
can scroll the deep rows into view.
- New `DocumentSidebar.layout.test.ts` source-string spec to lock
in the four cooperating bits and catch silent regressions.
* fix(core/voice): cancel during 'transcribing' actually drops the in-flight transcript
`cancel()` previously returned early for any state other than
`recording` / `starting`, so once the recording ended and the
network POST to the transcription endpoint started, the user's
cancel click was a no-op — the response would still arrive, get
inserted into the composer, and trip up the user.
- Add `transcribing` to the gating set in `cancel()` so a
cancel-while-transcribing reaches the `cancelledRef.current = true`
setter.
- In both the success and error branches of the fetch (`then` text
insert and `catch` live-snapshot fallback), check
`cancelledRef.current` immediately after the await. If true,
reset to idle and bail without forwarding the transcript to the
composer. This avoids both the 'cancelled but text still
appeared' bug and the analogous case where a live snapshot
fallback would write after the user cancelled.
* polish: composite-aware recorder dimensions, doc comment, calendar validator test
- templates/clips/app/components/recorder/recorder-engine.ts:
`readDimensions()` now prefers the combined stream's video track,
which under screen+camera mode is the composited
display+camera canvas (not the raw display feed). Keeps the
recorder's reported dimensions matching what's actually written
to disk.
- templates/clips/desktop/src/app.tsx: doc-comment refresh — the
camera stream isn't 'borrowed' for the video track anymore; it
feeds the composite that MediaRecorder reads from.
- templates/calendar/app/lib/event-form-utils.test.ts: vitest spec
for `getEventEndValidationMessage`, covering the equal-times
same-day case and the all-day end-before-start case.
* chore: add changeset for voice-dictation cancel-during-transcribe fix
* feat(core/composer): inline image attachments + attachment-only composer-mode sends + cancel active voice on submit
- PromptComposer: extract `buildPromptComposerSubmission()` helper
that flattens attachments into the outgoing message: pasted-text
files inline as fenced blocks, small text files inline via
`formatInlineTextFile`, image files inline via the new
`formatInlineImageFile` (data URL wrapped in an
`<uploaded-image name=... contentType=...>` block). Real File
attachments fall through as file uploads. Refactored
submission path consumes this helper so the same flattening
applies regardless of entry point.
- TiptapComposer: add a small `cancelActiveVoice()` helper called
on every send / intercept path so an in-progress voice
dictation gets cancelled when the user manually submits (was
previously inserting the late transcript on top of the just-
sent message). Pairs with the cancel-during-transcribe fix
shipped earlier on this branch.
- TiptapComposer: allow composer-mode submission with no text but
with attachments. Default to 'Use the attached context.' as the
prompt when text is empty; route the build through
`composerRuntime.setText` + `composerRuntime.send` (instead of
the plain `sendToAgentChat` helper) so the attachments survive
the wrap in the composer-mode prefix + `<context>` block.
* polish(clips): prettier reformat camera-composite + fall back to displayStream dimensions
- templates/clips/{app,desktop}/.../camera-composite.ts: prettier
reformat (no behavior changes — just rewrapping the long-line
ternary, object-type signature, and the error throw).
- templates/clips/desktop/src/lib/recorder.ts: when the primary
video track doesn't report `width`/`height` (some macOS
ScreenCaptureKit sources omit them under composite mode), fall
back to the underlying display stream's settings so the
persisted backup metadata still carries dimensions.
* chore: add changeset for composer inline-image attachments + voice-cancel-on-submit
* fix(core/AssistantChat): hide empty user-message bubble when only injected context remains
A user message whose text content was nothing but an injected
`<context>...</context>` block (sent via composer mode with only
attachments — e.g. "summarize this file" with no prose) would
render an empty grey bubble in the chat, since
`UserMessageText` strips the context tags before display.
- Extract `displayableUserMessageText()` helper so both
`UserMessageText` and `UserMessage` can ask 'is there anything
to show?' against the same stripping logic.
- `UserMessage` now computes `hasDisplayableText` over every text
part and:
- Skips rendering the bubble + copy-handler entirely when the
answer is no — only the attachment chips remain visible.
- Skips the height-measure effect and the expand/collapse
button so they don't run against a non-existent bubble.
- Pairs with the composer-side change shipped earlier on this
branch that allows attachment-only composer-mode sends.
* chore: add changeset for AssistantChat empty-bubble fix
* fix(security/sharing): scope owner-match by active orgId so personal queries don't leak org rows
`accessFilter()` and `resolveAccess()` both treated 'this row's
ownerEmail equals the current user' as sufficient proof of access,
without checking whether the resource's `orgId` matched the
request's active org context. A user with both a personal scope
and one or more org memberships could therefore see their org-
scoped resources while in personal scope, and vice versa — every
resource they had ever owned showed up regardless of the active
workspace.
- New `ownerScopeFilter()` helper in the SQL list path:
`ownerEmail = ? AND (orgId IS NULL OR orgId = ?)` keyed off
`ctx.orgId`. Personal scope (no `orgId`) emits
`orgId IS NULL`; org scope emits `orgId = ?` with the active
org. Wrapped in an AND with the existing owner-email clause.
- New `ownerMatchesActiveScope()` mirror for `resolveAccess()`'s
read-by-id path, applied to the early `role: 'owner'` return
so a stale share-link to a resource in another scope no longer
short-circuits the access check.
* test(core/sharing): cover orgId-scoped owner access for listVisible + resolveAccess
Companion spec for aa93f2f. Adds three rows covering each scope
(`owned` org=`orgId`, `owned-other-org`, `owned-solo` org=null)
and asserts that:
- `listVisible({userEmail, orgId})` only returns the matching-org
owned row plus normal org-share rows; cross-org rows are filtered out.
- `listVisible({userEmail, orgId: otherOrgId})` only returns the
other-org row; the personal-scope row stays hidden.
- `listVisible({userEmail})` (personal scope) only returns the
null-org row.
- `resolveAccess('doc-owned-other-org')` and
`resolveAccess('doc-owned-solo')` both return `null` from
inside the `{userEmail, orgId}` context where neither resource's
scope matches.
* test(core/composer): extract displayableComposerModeMessage helper + cover composer-mode + chat-display
- Extract `displayableComposerModeMessage()` from
`TiptapComposer`'s send path so the prefix-+-attachment fallback
('Use the attached context.') is a unit-testable function.
- `TiptapComposer.spec.ts` (modified) covers the three cases:
empty text + 0 attachments, empty text + attachments, non-empty
text.
- New `PromptComposer.spec.ts` covers
`buildPromptComposerSubmission()` — pasted-text inlining,
small-text-file inlining, image-file inlining via
`<uploaded-image>` block, and real File pass-through.
- New `AssistantChat.display.spec.ts` covers
`displayableUserMessageText()` — context-stripping, whitespace
trim, multiple-context-block stripping.
- `useVoiceDictation.ts` + `AssistantChat.tsx`: prettier-only
reformat (no behavior changes).
* fix(core/AssistantChat): correct two useEffect deps after empty-bubble fix
Two paired dependency fixes to the empty-bubble change I made in
091b9b8:
- `MarkdownText`'s `injectMarkdownStyles` effect doesn't depend on
`hasDisplayableText` — that variable isn't in scope and the
effect should only run once. Revert deps to `[]`.
- `UserMessage`'s height-measure ResizeObserver effect was missing
`hasDisplayableText` in its deps. It guards on
`hasDisplayableText` at the top, so the deps array should
re-run the effect when that flips.
* feat(clips): create-recording accepts spaceIds + wire through web + desktop + test
- `create-recording` action: new optional `spaceIds: string[]` arg
persisted via `stringifySpaceIds()` (already exported from
recordings server lib) so the recording row is attached to one
or more Spaces at creation time instead of after a follow-up
attach call.
- `templates/clips/app/routes/record.tsx`: pass `spaceIds: []` from
the live screen-recording and file-upload flows. Empty default
preserves existing behavior.
- `templates/clips/desktop/src/lib/recorder.ts`: same `spaceIds: []`
default in the Tauri recorder's POST body.
- `templates/clips/AGENTS.md`: `create-recording` arg row now
includes `--spaceIds`.
- `templates/clips/actions/create-recording.test.ts`: spec covers
passing a list of spaceIds and verifies they round-trip into
the persisted row's `space_ids` column.
* feat(content/list-documents): include accessRole/canEdit/canManage on each row
`list-documents` previously returned only document metadata,
leaving the UI to compute 'can I edit this doc?' separately or
optimistically — which led to subtle bugs where viewers saw edit
affordances they couldn't actually use.
The action now joins access-resolution into the listing:
- Owner detection: `ownerEmail === userEmail` AND owner-scope
matches the active orgId (mirrors the `accessFilter` change
in aa93f2f).
- Per-share lookup: a single bulk query against `documentShares`
fetches user + org grants for all returned documents in one
round-trip; `strongerRole()` picks the highest grant per
resource id.
- Visibility floor: org-scoped documents the viewer accesses via
visibility alone get a `viewer` role.
- Final shape adds `accessRole`, `canEdit`, `canManage` so the
sidebar / list UI can render the right affordances directly off
the listing response.
* feat(content/create-document): child docs inherit parent owner + org + visibility + shares
When a child document is created under a parent the current user
has editor access to, the child previously got created with
`ownerEmail = currentUser`, `orgId = currentUser's active orgId`,
and visibility `private` — so non-owner editors could create
children but never see them again (the access filter dropped
them), and org-shared parents produced personal-only children.
The action now:
- Reads the parent's `ownerEmail`, `orgId`, and `visibility` from
the access-checked parent resource and re-uses them for the new
child so the child lives in the same workspace + visibility
bucket as its parent.
- Copies every grant from `documentShares` for the parent onto
the child (same principal/type/role) so collaborators keep the
same access they had on the parent without a separate share
operation. `createdBy` records the actual creator.
- Returns `accessRole` / `canEdit` / `canManage` alongside the
document (matches the shape `list-documents` now returns in
f1d949b) so the UI doesn't have to re-resolve access right
after creation.
* fix(content/sidebar): optimistic delete + role-aware reorder buttons
- Move up / down arrows now only enable when both the current doc
and its neighbor have `canEdit`. Previously a viewer saw the
arrows enabled on any doc with siblings, then got a 403 toast
when they clicked. Uses the new `canEdit` field from
`list-documents` (f1d949b).
- Delete is now optimistic: `collectDocumentSubtreeIds()` finds
every descendant of the deleted node (no orphans), prunes them
from the React-Query cache before awaiting the mutation, and
navigates to a sensible next document (first favorite, else
first-by-position) if the current page is inside the deleted
subtree. `flushSync: true` so navigation completes before the
query cache invalidates and re-paints.
- On error the optimistic cache change is rolled back (full
invalidate), the user is navigated back to the original active
doc, and the toast surfaces the actual error message instead of
the generic 'Failed to delete page'.
* fix(content/sidebar): treat missing canEdit as editable + populate access fields on optimistic new-doc placeholder
Two small follow-ups to 19b8381 + c0d443c:
- Reorder availability flips the polarity slightly so a missing
`canEdit` (older cached payloads from before list-documents
started returning the field) is treated as editable rather than
not. Without this, the arrows momentarily disabled themselves
for everyone after the f1d949b deploy until the cache
refreshed.
- The optimistic placeholder row created during 'New document'
now carries `accessRole: 'owner'`, `canEdit: true`,
`canManage: true` so the sidebar's permission-aware affordances
light up immediately on the just-created row instead of
flickering between disabled and enabled while the server response
comes back.
* feat(design/systems): include access role + restrict selection actions to manageable systems
- `list-design-systems` action now resolves each row's access role
via `resolveAccess("design-system", id)` and exposes
`accessRole` + `canManage` (owner / admin only) on every
returned item, in both compact and full shapes. The bulk
parallelization is intentional — the access check is in-process
and the list payload is small.
- `DesignSystems` page: selection-mode and bulk-delete affordances
are now gated on `canManage`. A viewer/editor of a shared design
system sees the system in the list but can't tick its checkbox,
trigger the master 'select all', or open the Select header
button at all when nothing in the list is theirs to manage.
`toggleSystemSelection` early-returns when the row isn't
manageable, and the deps array is updated accordingly.
* fix(design/systems): surface actual error in delete toasts
Both single-delete and bulk-delete handlers swallowed the error
and only invalidated the cache. Users saw the row reappear after
the optimistic remove but got no toast explaining why. Both
handlers now also fire a sonner `toast.error` with the underlying
error message in the description so the failure is visible.
* style(design/systems): prettier reformat of toggleSystemSelection useCallback
Prettier rewrapped the inline-arrow-with-deps useCallback into the
multi-line form. No behavior change.
* fix(core/sharing-spec): preserve explicit null orgId in insertDoc + alphabetical assertion
Two follow-ups to 8337a53:
- `insertDoc` was applying `values.orgId ?? orgId` to its inputs,
so an explicit `orgId: null` argument was being silently replaced
by the test fixture's default `orgId`. The `owned-solo`
(intentionally `orgId: null`) row was therefore being inserted
with the wrong scope, masking the regression the test is meant
to catch. Switch to the `=== undefined` check so explicit
`null` survives.
- Other-org listVisible assertion: list comes back ordered by id,
so the `owned-other-org` / `other-org` pair needs to be in that
order alphabetically — flip to match.
- Prettier-only reformat of the assertion against
`resolveAccess("doc-owned-other-org")`.
* fix(content/sidebar): gate per-row actions on canEdit + canManage
Continues the access-aware sidebar work earlier on this branch.
Each tree row now reads `canEdit` and `canManage` (admin/owner)
from the document and shows only the affordances it owns:
- `hasMenuActions`: hide the entire `⋯` dropdown trigger when the
row has neither edit nor manage capability (viewer-only access).
- `canEdit`: Add sub-page, Move up, Move down, and Favorite items
only render for editors. The hover `+` quick-action next to the
`⋯` trigger is also gated here.
- `canManage`: only owners/admins see the destructive Delete row.
- The separator between the editor block and the manage block only
renders when both are present, so the menu doesn't show a
trailing separator when only one of the two is available.
* test(content/sidebar): lock in canEdit/canManage gating in DocumentTreeItem
Source-string spec ensures the per-row `canEdit` / `canManage`
gating from 091c889 stays wired up — the source must keep
`const canEdit = node.canEdit !== false`, `const canManage =`,
and the two conditional render guards, so future refactors don't
silently re-expose Add/Move/Delete to viewers.
* fix(slides/DeckContext): backfill open deck from /api/decks/:id if listing omitted it
The list endpoint scopes results by the user's active org +
sharing grants, which means a deck the user opens via a direct
URL (e.g. someone shared a link cross-org, or visibility flipped
between scopes) can be missing from the list payload — and the
editor would render an empty deck list with the URL still
pointing at a non-existent deck id.
- New `deckIdFromPathname()` parses the deck id from
`/deck/<id>` with URL-decoding fallback.
- New `includeOpenDeckIfMissing()` accepts the loaded list, the
currently-open deck id (from `window.location.pathname`), and
a fetcher that defaults to `fetchDeckFromAPI(id)`. If the open
id isn't in the list, it does a single GET against
`/api/decks/:id` and appends the deck if it comes back. The
fetcher param is wired up so unit tests can stub it.
- Used in the initial `useEffect` mount path so the editor sees
the open deck on first paint instead of waiting for the
fallback poll.
- The existing `readOpenDeckId` closure now delegates to
`deckIdFromPathname()` to share the same parsing rules.
* test(slides/DeckContext): cover deckIdFromPathname + includeOpenDeckIfMissing helpers
Companion spec for a493046. Covers:
- `deckIdFromPathname`: editor route, present sub-route,
URL-encoded id, and a non-matching path returning null.
- `includeOpenDeckIfMissing`: backfills the open deck when the
list omits it (and only then — no extra fetch when the list
already contains the open deck).
* fix(content/p.$id): render private-document notice for signed-in viewers without access
Previously a signed-in user who didn't have access to a non-public
document got a generic 404 — even though the loader knows the doc
exists, it just isn't theirs to see. That made shared-link
mishaps confusing ('the link is broken' vs 'I don't have
access').
- Loader now returns `{ document: null, unavailable: { reason: 'private' } }`
for the signed-in-no-access branch instead of throwing 404.
- New `PrivateDocumentNotice` component renders a centered card
with an `IconLock`, a clear 'This document is private'
headline, and a one-line ask-the-owner-to-share hint. Same
empty-state visual language as the rest of the template's
unavailable states.
- `PublicDocumentPage` returns the notice when `document` is
null. The signed-out branch already redirects to sign-in
(unchanged) and the genuinely-missing branch still throws 404.
* feat(content/editor): inline /generate <prompt> shortcut + extracted parser
Lets users type `/generate <prompt>` mid-document and hit Enter
to fire the agent generation without opening the slash command
menu — same flow as the menu-driven Generate item, just without
the popover step.
- New `parseInlineGeneratePrompt(textBeforeCursor)` helper
matches `/^/generate\s+([\s\S]+)$/` and returns the trimmed
prompt or null. Exported so the spec can pin the grammar.
- `readInlineGenerateCommand()` walks the current editor state,
bails on non-empty selection or non-textblock parents, and
returns the prompt-range pair if the line is a
`/generate <prompt>` command.
- `submitGeneratePrompt(prompt)` shares the trimmed-prompt
+ document-context send path with the existing menu Generate
item — toast on missing documentId, fire `send()` with the same
update-document instructions.
- New `SlashCommandMenu.test.ts` covers the parser: happy path,
whitespace trim, and three rejection cases (`/generate` with
no prompt, a different slash command, and `/generate` not at
the start of the line).
* fix(content/editor): preserve editor focus when clicking slash-command items
`onMouseDown` now `preventDefault()`s so clicking a slash-menu
item doesn't blur the editor before the click handler runs. The
command's editor transactions (e.g. set heading, insert table)
depend on the editor still owning the selection — without this
the click landed after focus had moved to the menu button, the
range was lost, and the inserted node was attached to the wrong
position.
* fix(clips/record): drop duplicate spaceIds key in create-recording POST bodies
Two call-sites in record.tsx (live recording + file upload) had
both an empty `spaceIds: []` (from my earlier 91b685f commit
that wired through the schema default) AND a later
`spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined` from
the concurrent agent's work that reads the active space from the
URL. Result was a duplicate-key object literal that TypeScript
hard-errors (`TS1117`) and prettier was quiet about. Keep the
URL-aware spaceIds variant — it's the actual feature wiring — and
drop the trivial `[]` defaults.
* style: prettier reformat of use-db-sync, access, and list-documents
Pure formatting churn from running prettier — three call sites
got reflowed without behavior changes:
- `use-db-sync`: `hasAppStateEvent` predicate joined to one line.
- `access.ts`: `accessFilter` owner clause split across more
arguments per line.
- `list-documents`: `strongerRole` signature reflowed to single line.1 parent 4f8e18f commit 2eb5064
54 files changed
Lines changed: 2640 additions & 409 deletions
File tree
- .agents/skills/address-feedback
- .changeset
- packages/core/src
- client
- composer
- server
- sharing
- templates
- calendar/app
- components
- calendar
- layout
- lib
- clips
- actions
- app
- components/recorder
- lib
- routes
- desktop/src
- lib
- content
- actions
- app
- components
- editor
- sidebar
- ui
- routes
- design
- actions
- app/pages
- images/server/lib
- slides/app
- components
- editor
- presentation
- context
- lib
- pages
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| 73 | + | |
73 | 74 | | |
74 | 75 | | |
75 | 76 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1713 | 1713 | | |
1714 | 1714 | | |
1715 | 1715 | | |
1716 | | - | |
1717 | | - | |
1718 | | - | |
| 1716 | + | |
1719 | 1717 | | |
1720 | 1718 | | |
1721 | 1719 | | |
| |||
1779 | 1777 | | |
1780 | 1778 | | |
1781 | 1779 | | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
1782 | 1784 | | |
1783 | 1785 | | |
1784 | 1786 | | |
| |||
1837 | 1839 | | |
1838 | 1840 | | |
1839 | 1841 | | |
1840 | | - | |
| 1842 | + | |
| 1843 | + | |
| 1844 | + | |
| 1845 | + | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
1841 | 1851 | | |
1842 | 1852 | | |
1843 | 1853 | | |
1844 | | - | |
| 1854 | + | |
1845 | 1855 | | |
1846 | 1856 | | |
1847 | 1857 | | |
| |||
1851 | 1861 | | |
1852 | 1862 | | |
1853 | 1863 | | |
1854 | | - | |
| 1864 | + | |
1855 | 1865 | | |
1856 | 1866 | | |
1857 | 1867 | | |
| |||
1860 | 1870 | | |
1861 | 1871 | | |
1862 | 1872 | | |
1863 | | - | |
1864 | | - | |
1865 | | - | |
1866 | | - | |
1867 | | - | |
1868 | | - | |
1869 | | - | |
1870 | | - | |
1871 | | - | |
1872 | | - | |
1873 | | - | |
1874 | | - | |
1875 | | - | |
1876 | | - | |
1877 | | - | |
1878 | | - | |
1879 | | - | |
| 1873 | + | |
1880 | 1874 | | |
1881 | | - | |
1882 | | - | |
1883 | | - | |
1884 | | - | |
1885 | | - | |
| 1875 | + | |
| 1876 | + | |
| 1877 | + | |
| 1878 | + | |
| 1879 | + | |
| 1880 | + | |
| 1881 | + | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
| 1891 | + | |
1886 | 1892 | | |
1887 | | - | |
1888 | | - | |
1889 | | - | |
1890 | | - | |
1891 | | - | |
| 1893 | + | |
| 1894 | + | |
| 1895 | + | |
| 1896 | + | |
| 1897 | + | |
| 1898 | + | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
| 1905 | + | |
| 1906 | + | |
| 1907 | + | |
| 1908 | + | |
1892 | 1909 | | |
1893 | | - | |
1894 | | - | |
1895 | | - | |
1896 | | - | |
1897 | | - | |
| 1910 | + | |
| 1911 | + | |
1898 | 1912 | | |
1899 | 1913 | | |
1900 | 1914 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
0 commit comments