Skip to content

refactor(web): eliminate lib API files, DRY up deploy flow, extract library sub-components (LUM-1964)#32286

Merged
ashleeradka merged 2 commits into
mainfrom
devin/1779908910-lum-1964-eliminate-lib-api-files
May 27, 2026
Merged

refactor(web): eliminate lib API files, DRY up deploy flow, extract library sub-components (LUM-1964)#32286
ashleeradka merged 2 commits into
mainfrom
devin/1779908910-lum-1964-eliminate-lib-api-files

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 27, 2026

Prompt / plan

Eliminate the three remaining hand-written API wrapper files (lib/apps-api.ts, lib/documents-api.ts, lib/publish-api.ts) by migrating all consumers to the generated daemon SDK. Then address the deploy/publish flow duplication and architectural issues discovered during code review.

Closes LUM-1964

What changed

1. Eliminate hand-written API wrappers (commit 1)

Deleted 3 files (552 lines of wrapper code), migrated 14 consumer files:

Deleted file Replaced by
lib/apps-api.ts Direct SDK calls (appsByIdOpenPost, appsByIdDeletePost), TanStack Query hooks (appsGetOptions), utility extractions (utils/app-html-cache.ts, utils/share-app.ts, utils/import-bundle.ts)
lib/documents-api.ts Direct SDK calls, TanStack Query hooks (documentsGetOptions), type exports (types/document-types.ts)
lib/publish-api.ts Shared utility (utils/publish-app.ts), type exports (types/publish-types.ts)

2. DRY up deploy flow + fix regressions (commit 2)

Bug fixes:

  • Restore publish-status fallback — The old publishApp() wrapper performed a best-effort getPublishStatus() call when the publish endpoint returned success: true but omitted publicUrl. All 4 call sites were missing this enrichment after the migration. Created utils/publish-app.ts with a shared publishApp() utility that restores this behavior.
  • Fix stale HTML cache after app deletionhandleConfirmDelete was missing the clearAppHtmlCache() call that the old deleteApp() wrapper performed, causing deleted apps to leave stale Promise entries in the in-memory cache.

Architecture improvements:

  • Move deploy-store.ts to stores/ — It was in domains/chat/ but is cross-domain shared state used by both chat-page.tsx and library-view.tsx. Per STATE_MANAGEMENT.md, cross-domain Zustand stores belong in stores/.
  • Consolidate library-view deploy statelibrary-view.tsx had 4 local useState hooks (isDeploying, showTokenDialog, pendingDeployAppId, complexDeployApp) duplicating state already managed by the deploy store. Now consumes useDeployStore directly.
  • DRY up publish result toasts — 5 identical toast-handling blocks (3 cases each: success+URL, success-no-URL, error) consolidated into showPublishResultToast() in the deploy store.

Component extraction (library-view.tsx 916 → 500 lines):

  • library-app-card.tsxLibraryAppCard + LibraryAppCardActionsMenu (desktop dropdown + mobile bottom sheet)
  • library-document-card.tsxLibraryDocumentCard
  • library-date.ts — shared formatLibraryDate() utility

Test plan

  • bunx tsc --noEmit — 0 type errors
  • bun run lint — 0 errors
  • bun run build — succeeds
  • bun run audit:cross-domain:check — passes
  • bun test — 58 tests pass across 4 test suites:
    • deploy-store.test.ts (10/10) — moved to stores/, import path updated
    • viewer-store.test.ts (29/29)
    • app-pin-storage.test.ts (15/15)
    • dynamic-page-surface.test.tsx (4/4) — mock path updated for app-html-cache

Link to Devin session: https://app.devin.ai/sessions/4b3b130bbf274e5487da3b4992883093
Requested by: @ashleeradka

…pi.ts (LUM-1964)

Replace hand-written API wrappers with generated daemon SDK calls and
TanStack Query hooks. Business logic (HTML cache, share-app download
chain, import-bundle binary upload, credential error detection) moves
to purpose-built utility files under utils/ and types/.

Consumers that listed apps/documents now use generated query hooks
(appsGetOptions, documentsGetOptions) with select transforms. Mutation
consumers call the generated SDK functions directly and invalidate
the query cache.

Closes LUM-1964

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@linear
Copy link
Copy Markdown

linear Bot commented May 27, 2026

LUM-1964

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 067d675c45

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +140 to +143
const { data: result } = await appsByIdPublishPost({
path: { assistant_id: assistantId, id: appId },
throwOnError: true,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the publish-status fallback after publishing

When /publish returns { success: true } without an inline publicUrl (the response schema allows this, and the removed publishApp() wrapper explicitly handled it), this path now shows a generic success toast and never looks up /publish-status, so users lose the Open action even though the deployment record may already contain the URL. Please preserve the old best-effort status lookup here; the same direct appsByIdPublishPost pattern in LibraryView should be updated as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread apps/web/src/components/apps/library-view.tsx
Comment thread apps/web/src/domains/chat/deploy-store.ts Outdated
…xtract sub-components

- Create utils/publish-app.ts: shared publishApp() with best-effort
  status enrichment when publicUrl is missing from publish response
  (restores behavior dropped from lib/publish-api.ts)
- Move deploy-store.ts from domains/chat/ to stores/ (cross-domain
  shared state used by both chat-page and library-view)
- Consolidate library-view.tsx to use deploy store instead of 4 local
  useState hooks for deploy state (isDeploying, tokenDialog, etc.)
- Extract LibraryAppCard, LibraryDocumentCard, LibraryAppCardActionsMenu
  into dedicated component files (library-view 916→500 lines)
- Fix clearAppHtmlCache not called after app deletion (stale cache bug)
- DRY up publish result toast (5 identical blocks → showPublishResultToast)

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration devin-ai-integration Bot changed the title refactor(web): eliminate lib/apps-api.ts, documents-api.ts, publish-api.ts (LUM-1964) refactor(web): eliminate lib API files, DRY up deploy flow, extract library sub-components (LUM-1964) May 27, 2026
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — reviewed at commit 067d675c45

This is solid SDK elimination + DRY refactor work. Architecture violations (multi-writer Zustand, hand-coded wrappers, missing publish-status fallback) are all fixed. The extracted components are simple presentation — state concerns remain at the parent level.


✅ Key wins

SDK elimination: All 3 wrapper files deleted (552 lines). 14 consumer migrations to daemon SDK + TanStack Query looked good — especially the shift from manual fetch loops to React Query's built-in caching + invalidation. The generated SDK + heyapi bindings are working.

publish-app.ts recovery: The old publishApp() wrapper did a best-effort getPublishStatus() follow-up when publicUrl was missing from the publish response. All 4 call sites lost this enrichment in the initial SDK migration. You've restored it into utils/publish-app.ts and wired it back into deploy-store.ts. No callers were passing preserveOnFailure=true, so zero regression.

Extraction quality: LibraryAppCard and LibraryDocumentCard are pure presentation — they accept data + callbacks (onPin, onDelete, onDeploy, onOpen), render UI, and delegate. Parent (library-view.tsx) owns the list state via useQuery. Clean separation.

deploy-store relocation: Moved from domains/chat/ to stores/ because it's now used by both chat app viewer AND library page. Already wrapped with createSelectors so atomic .use.*() subscribers won't cause re-renders on unrelated field changes. ✅

ConversationAssetsPill refactor: The old useEffect had manual fetch + cancelled guard + error catch. Converted to useQuery + invalidateQueries. Same cancellation semantics, cleaner code, TanStack Query's built-in stale-time management. The select: (data) => data.apps transform is correct.


⚠️ Watch spots for QA

  1. appsByIdDeletePost in library-view — The old deleteApp() wrapper may have had error handling or retry logic. Verify the direct SDK call has parity.

  2. conversation-assets-pill.tsx select transformsselect: (data) => data.apps and select: (data) => data.documents are dropping the container object. If the API ever returns null for these fields, the transform turns it into undefined. Check: are apps and docs always arrays in the daemon response schema, never null?

  3. deploy-store dual-use state reset — This store is now mounted in two independent route contexts (chat app viewer + library page). Zustand module-level singletons survive route changes by design. Verify:

    • Opening chat app viewer, deploying something, then navigating to library page — does library page see stale pendingDeployAppId or isDeploying? (It shouldn't.)
    • Test the full flow: library page → deploy app #1 → open it in chat → cancel midway → nav back to library → try deploying app #2. State should reset cleanly.
  4. Extracted component event listeners — The old library-view.tsx may have had cleanup for document/window listeners (resize, scroll). Verify LibraryAppCard doesn't attach listeners that the extracted version doesn't clean up.


Pattern compliance

Pattern Status
Zustand createSelectors ✅ deploy-store already uses it
TanStack Query options pattern appsGetOptions(), documentsGetOptions() used correctly
Effect cancellation ✅ Manual cancelled guard → React Query's built-in cleanup
Error handling — async effects ✅ Preserved during useEffect → useQuery migration
Module-level singletons ✅ Zustand stores created at module scope, not in components

No anti-pattern violations found. The refactor is architecturally sound.


Merge criteria

Codex + Devin have reviewed. Given the relocation of deploy-store to shared state + its use in a new context (library page), suggest:

  1. Run QA on deploy/share flows in BOTH chat app viewer AND library list (watch spot #3).
  2. Verify app deletion in library-view doesn't have UX regressions vs. old wrapper (watch spot #1).
  3. Confirm type safety of select transforms — are array-or-null fields handled safely (watch spot #2)?

Then merge.

Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — reviewed at commit 067d675c45

This is solid SDK elimination + DRY refactor work. Architecture violations (multi-writer Zustand, hand-coded wrappers, missing publish-status fallback) are all fixed. The extracted components are simple presentation — state concerns remain at the parent level.


✅ Key wins

SDK elimination: All 3 wrapper files deleted (552 lines). 14 consumer migrations to daemon SDK + TanStack Query looked good — especially the shift from manual fetch loops to React Query's built-in caching + invalidation. The generated SDK + heyapi bindings are working.

publish-app.ts recovery: The old publishApp() wrapper did a best-effort getPublishStatus() follow-up when publicUrl was missing from the publish response. All 4 call sites lost this enrichment in the initial SDK migration. You've restored it into utils/publish-app.ts and wired it back into deploy-store.ts. No callers were passing preserveOnFailure=true, so zero regression.

Extraction quality: LibraryAppCard and LibraryDocumentCard are pure presentation — they accept data + callbacks (onPin, onDelete, onDeploy, onOpen), render UI, and delegate. Parent (library-view.tsx) owns the list state via useQuery. Clean separation.

deploy-store relocation: Moved from domains/chat/ to stores/ because it's now used by both chat app viewer AND library page. Already wrapped with createSelectors so atomic .use.*() subscribers won't cause re-renders on unrelated field changes. ✅

ConversationAssetsPill refactor: The old useEffect had manual fetch + cancelled guard + error catch. Converted to useQuery + invalidateQueries. Same cancellation semantics, cleaner code, TanStack Query's built-in stale-time management. The select: (data) => data.apps transform is correct.


⚠️ Watch spots for QA

  1. appsByIdDeletePost in library-view — The old deleteApp() wrapper may have had error handling or retry logic. Verify the direct SDK call has parity.

  2. conversation-assets-pill.tsx select transformsselect: (data) => data.apps and select: (data) => data.documents are dropping the container object. If the API ever returns null for these fields, the transform turns it into undefined. Check: are apps and docs always arrays in the daemon response schema, never null?

  3. deploy-store dual-use state reset — This store is now mounted in two independent route contexts (chat app viewer + library page). Zustand module-level singletons survive route changes by design. Verify:

    • Opening chat app viewer, deploying something, then navigating to library page — does library page see stale pendingDeployAppId or isDeploying? (It shouldn't.)
    • Test the full flow: library page → deploy app #1 → open it in chat → cancel midway → nav back to library → try deploying app #2. State should reset cleanly.
  4. Extracted component event listeners — The old library-view.tsx may have had cleanup for document/window listeners (resize, scroll). Verify LibraryAppCard doesn't attach listeners that the extracted version doesn't clean up.


Pattern compliance

Pattern Status
Zustand createSelectors ✅ deploy-store already uses it
TanStack Query options pattern appsGetOptions(), documentsGetOptions() used correctly
Effect cancellation ✅ Manual cancelled guard → React Query's built-in cleanup
Error handling — async effects ✅ Preserved during useEffect → useQuery migration
Module-level singletons ✅ Zustand stores created at module scope, not in components

No anti-pattern violations found. The refactor is architecturally sound.


Merge criteria

Codex + Devin have reviewed. Given the relocation of deploy-store to shared state + its use in a new context (library page), suggest:

  1. Run QA on deploy/share flows in BOTH chat app viewer AND library list (watch spot #3).
  2. Verify app deletion in library-view doesn't have UX regressions vs. old wrapper (watch spot #1).
  3. Confirm type safety of select transforms — are array-or-null fields handled safely (watch spot #2)?

Then merge.

Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — reviewed at 143c8ca9

Value: Engineers and users get a codebase with 552 fewer lines of hand-rolled API glue, two real runtime bugs fixed (stale HTML cache + missing publish URL), and a cleaner state topology — all with passing TypeScript, tests, and the cross-domain audit.

Full analysis

Bug fixes verified (commit 2)

Both Devin/Codex findings from 067d675c were genuinely important regressions — and both fixes are correct.

1. utils/publish-app.ts — publish-status enrichment restored ✅
The best-effort follow-up to appsByIdPublishstatusGet when result.success && !result.publicUrl is correctly wired: empty catch {} is right here (still returns the publish result; user sees "Deployed to Vercel" without the Open action rather than an error). One minor note: result.publicUrl = status.publicUrl mutates the API response object in-place. Functionally fine — generated SDK responses are plain objects — but returning { ...result, publicUrl: status.publicUrl } would be the more conventional immutable pattern. Non-blocking.

2. clearAppHtmlCache after deletion — stale cache fix ✅
clearAppHtmlCache is correctly imported in library-view.tsx and called in handleConfirmDelete. The cache-aside pattern in app-html-cache.ts is clean: it deletes the key on fetch failure too, which is the right error-cleanup behavior.

Architecture improvements

deploy-store.tsstores/ — Correct. It's read by both chat-page.tsx and library-view.tsx. Per STATE_MANAGEMENT.md, cross-domain shared state belongs in stores/. Doc comment updated to reflect the new rationale. ✅

showPublishResultToast() — DRYs 2 identical 8-line conditional blocks in the deploy flow. The result flowing in has already been enriched by publishApp(), so result.publicUrl is populated wherever possible. ✅

library-view.tsx 916 → ~500 lines — The 4 local useState hooks (isDeploying, showTokenDialog, pendingDeployAppId, complexDeployApp) that duplicated deploy store state are correctly removed; the file now delegates to useDeployStore directly. Clean separation.

New utilities

File Assessment
utils/app-html-cache.ts Clean cache-aside, error-cleanup on failure, primeAppHtmlCache for pre-warming ✅
utils/share-app.ts Stream download via parseAs: "stream" correct for binary blobs ✅
utils/import-bundle.ts Uses raw daemonClient for binary upload — comment correctly explains why generated SDK can't handle body?: never for octet-stream ✅
utils/publish-app.ts See bug fix note above ✅
types/app-types.ts, document-types.ts, publish-types.ts Clean re-exports; isCredentialError correctly lives in publish-types.ts

Extracted sub-components

LibraryAppCard and LibraryDocumentCard are textbook extractions — focused props interfaces, no leaked internal state, formatLibraryDate shared utility. handleShare includes isSharing in useCallback deps (correct for the if (isSharing) return guard; minor callback recreation on state change, acceptable for a non-hot path). formatLibraryDate creates new Date() per call for year comparison — fine for a display utility.

conversation-assets-pill.tsx

Migration from hand-rolled useState/useEffect/fetch pattern to TanStack Query options from the generated SDK is correct and consistent with the rest of the PR's direction. useMemo for derived assets array is appropriate.

Merge gate

  • ✅ Vex APPROVE (this review, 143c8ca9)
  • ⚠️ Devin reviewed 067d675c (stale) — two inline confirmations posted at 143c8ca9 but review bodies are empty (not "No Issues Found"). 6 additional findings are hidden in the Devin app and weren't surfaced here.
  • ⚠️ Codex reviewed 067d675c (stale) — no 👍 or "No major issues" at HEAD.

Recommend: trigger @devin review this PR + @codex review for clean second approval at 143c8ca9. Devin's 6 hidden findings at the first commit should be surfaced — worth knowing if any survived into the second commit.

Vellum Constitution — Trust-seeking: two silent runtime bugs (missing cache eviction, dropped deploy URL) caught and verifiably fixed before they reached users.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ashleeradka ashleeradka merged commit 3bbc4a7 into main May 27, 2026
7 checks passed
@ashleeradka ashleeradka deleted the devin/1779908910-lum-1964-eliminate-lib-api-files branch May 27, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant