Repository navigation
feat: publish extensions overhaul - #2098
Conversation
There was a problem hiding this comment.
Pull request overview
This PR overhauls extension publishing in the Web UI by replacing the per-extension publish dialog with a dedicated /publish page and a global drag-and-drop publishing flow backed by a persistent publish queue context.
Changes:
- Add a
/publishpage with a multi-file picker + drop area that immediately enqueues/uploads.vsixpackages and shows progress as an inline card strip. - Introduce an app-wide publish queue context with polling to reflect post-upload outcomes (review verdicts, icon availability).
- Turn the navbar “Publish” control into an exported
PublishButtonthat combines link +pshortcut + drop target.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
Suppressed comments (3)
webui/test/unit/context/publish-queue-context.spec.tsx:102
publishExtensionshould resolve to anExtension, not an array. Using[published()]here can cause the publish queue to poll for up to 60s (becausefiles.iconis missing on an array), making this test slow/flaky.
it('creates the namespace when the error comes back as a value instead of a throw', async () => {
const publishExtension = vi
.fn()
.mockResolvedValueOnce({ error: 'Unknown publisher: foo\nUse the CLI to create it' })
.mockResolvedValueOnce([published()]);
const createNamespace = vi.fn().mockResolvedValue({ success: 'ok' });
webui/test/unit/context/publish-queue-context.spec.tsx:116
- Same issue as above:
publishExtensionshould resolve to a singleExtension(or reject withErrorResult), not[Extension]. Returning an array here can trigger the queue’s polling loop and make the test hang/flap.
it('reads the namespace even when the message carries no second line', async () => {
const publishExtension = vi
.fn()
.mockRejectedValueOnce({ error: 'Unknown publisher: foo' })
.mockResolvedValueOnce([published()]);
const createNamespace = vi.fn().mockResolvedValue({ success: 'ok' });
webui/test/unit/context/publish-queue-context.spec.tsx:152
publishExtensionresolves to anExtension, not an array. Returning[published()]here can keep the item inawaitingIconpolling for up to 60s, making the test unnecessarily slow/flaky.
const publishExtension = vi
.fn()
.mockResolvedValueOnce([published()])
.mockReturnValueOnce(new Promise(() => {}));
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
netomi
left a comment
There was a problem hiding this comment.
Nice feature — reviewed the queue/poll logic and the drop-target wiring in detail. Found one feature-breaking gap and a few real logic bugs in the poll/queue state machine, plus some lower-priority efficiency and duplication notes. All confirmed by reading the code directly (not just tool output). Inline comments below; a couple more that don't anchor to one line:
- No client-side file-size check before upload (a regression from the deleted
publish-extension-dialog.tsx, which used react-dropzone'smaxSizeto reject oversized files instantly).publish()(publish-queue-context.tsx:268) only filters onisVsixFile; an oversized file now fully uploads before the server rejects it. - Every concurrent upload polls independently — each
pollUntilSettledcall hits the full extensions-list endpoint on its own 5s timer, so N concurrent items means N× redundant full-list GETs every tick instead of one shared read. - No upload concurrency cap —
publish()'squeued.forEach(({ id, file }) => upload(id, file))(publish-queue-context.tsx:279) fires every queued file's upload simultaneously with no batching/throttling. - Smaller/lower-priority, not blocking:
statusOf()duplicatesgetExtensionStatus's precedence logic (risk of divergence);errorMessage()diverges fromutils.ts'shandleError(drops the.messagehalf of combined error objects); the hand-rolled drag-depth tracking inuse-publish-drop.tsreimplements whatreact-dropzonealready did — and that package is now an unused dependency left inpackage.json;dismiss()is exported/wired but never called from any UI; the newformatFileSizeutil has no test coverage; the newiconPendingprop onExtensionCardPropsisn't reflected in the existing unreleased CHANGELOG bullet for that interface.
|
I will migrate the publish to tanstack as well, just to avoid the abort that doesn't make a lot of sense here |
b58f605 to
a960230
Compare
8867460 to
8c6b3c9
Compare

Replaces the one-file-at-a-time publish dialog with a /publish page and a drag-and-drop flow. Dragging a file anywhere turns the navbar's Publish button into a drop area; everything dropped on it is uploaded straight away, no confirmation. The queue lives in a context, so it survives navigation, and renders as a line of extension cards that poll until each package settles.
The button is exported as PublishButton — it carries the link, the p shortcut and the drop target together, so a deployment with its own menu content keeps all three.
Screen.Recording.2026-08-25.at.13.07.46.mov