The agent could only ever see the last-messaged tab because saved tabs
had no id the agent service recognizes. The Cloud Ingest workflows API
turns out to be that id space (live-verified: its ids are accepted by
the messages and draft endpoints), so every message now refreshes a
name-to-id index from GET /workflows (paginated, capped, warn on
truncation) and resolves each open saved tab through it, with the
ack-binding store as fallback for unsaved tabs. Reverse resolution
activates the existing tab on switch_tab instead of minting a copy.
Ambiguity never guesses: duplicate or nameless cloud records and
same-named open tabs are excluded from resolution and fall back to
path-exact bindings. The per-send refresh is raced against a timeout
so a slow index can never block sending.
Draft-bearing sends that 5xx now retry once without the draft so a
drafts outage degrades to plain chat, re-arming the upload for the
next message.
The frontend sent the graph JSON's internal id as a speculative
workflow_id, which the backend never issued, so every unbound send
403'd before the retry stripped it. Remove the guess entirely: bound
tabs send their bound id, unbound tabs send none and adopt through the
uploaded draft.
Dedup draft uploads by content and tab path so a Save As still uploads,
and reclaim the binding when an unbound active tab's canvas exactly
matches the last graph known for a workflow, so renamed tabs keep their
workflow identity instead of hijacking the focused one. The reclaim
fingerprint resets with the other guards and is consumed on use.
Move the cascading rules (keyframes, .agent-shimmer-text, scoped reset)
from globally imported agentTheme.css into agentPanel.css, loaded only
by the lazy AgentPanelRoot chunk; agentTheme.css keeps the @theme blocks.
Dist grep: cascading rules land in AgentPanelRoot-*.css only, not entry css.
Implement the tabs and activity spec end to end:
- Handle agent_active_tab: activate the bound tab for the workflow, or
mint, open, and autosave a named temporary tab for an unknown one.
Draft bases adopt without re-rendering versions already on canvas,
and concurrent focus events serialize latest-wins.
- Send open_tabs and current_tab with every message so the agent can
address the user's open workflows by name and switch between them.
- Persist the workflow-tab binding map in localStorage so bindings
survive reloads and backgrounded tabs stay addressable.
- Render friendly labels for new_tab, switch_tab, remember, and forget
tool calls; unknown tools keep their raw name via a prototype-safe
lookup.
## Summary
Auth error toasts in the signup/login flow (e.g. "No account found with
this email…") were never tracked, leaving no analytics visibility into
signup dropoff. This adds a PostHog event for them.
## Changes
- **What**: New `app:user_auth_failed` PostHog event with `{ error_code,
auth_action }`, fired from a `reportAuthFlowError` wrapper in
`useAuthActions` before the existing error toast. Wired to the seven
auth-flow actions (`email_sign_in`, `email_sign_up`, `google_sign_in`,
`google_sign_up`, `github_sign_in`, `github_sign_up`, `password_reset`);
covers both the cloud login/signup pages and the in-app sign-in dialog
since all funnel through the same handler. Non-auth actions (logout,
billing) intentionally stay untracked.
## Review Focus
- PostHog provider only — not added to Mixpanel/GTM/CustomerIo/etc.
- No PII: only the Firebase error code is sent (`'unknown'` for
non-Firebase errors), never the message or email.
- The `useAuthActions` test mock of `wrapWithErrorHandlingAsync`
previously rethrew errors; the real implementation swallows them after
handling. The mock now matches the real contract, and one pre-existing
logout assertion that depended on the rethrow was updated.
Fixes
[MAR-451](https://linear.app/comfyorg/issue/MAR-451/add-tracking-for-signup-flow-errors)
---------
Co-authored-by: Deep Mehta <42841935+deepme987@users.noreply.github.com>
## Summary
Switch the meta title separator sitewide from an em dash (—) to a spaced
hyphen ( - ), the follow-up to #13480 tracked as FE-1233, based on the
Screaming Frog crawl that flagged the em dash titles.
## Changes
- **What**: Replaced the em dash separator with a hyphen in every page
meta title, in both `en` and `zh-CN`. Covers ~44 inline page titles, the
shared title keys in `i18n/translations.ts` (`cloudNodes.meta.title`,
`mcp.meta.title`, `affiliate-terms.page.title`,
`enterprise-msa.page.title`, `affiliate.page.title`,
`launches.page.title`), and the dynamic `customers`, `demos`, and
`supported-models` `... - Comfy` suffixes. `BaseLayout` reuses each page
title for `og:title`, `twitter:title`, and the JSON-LD `name`, so those
inherit the change from a single source per page.
- Updated the e2e title assertions to match. `launches` asserts via
`t()` so it follows the translation change automatically.
## Review Focus
- **Scope is titles only.** The crawl and this ticket are about meta
titles. Description / body copy em dashes are left untouched, they are
marketing copy, which we keep em-dashed per the comms-only styling
decision. A full JSON-LD audit across all 57 routes confirms every
`name`/title field is now em-dash-free.
- **zh-CN cloud-nodes title keeps its Chinese double dash `——`**
(`自定义节点包合集——开箱即用`). That is correct CJK punctuation, a dash break, not a
brand/Latin separator, so the Latin-script separator concern does not
apply. Its e2e assertion is left as `——` to match.
- No visual change: titles are `<head>` metadata, not rendered UI, so
there is nothing to screenshot on the page. Rendered `<title>` output
was verified on the dev server across inline, `t()`-based, zh-CN,
dynamic, and 404 routes, and `og:`/`twitter:` inheritance was confirmed.
Ref: FE-1233
## Summary
App mode now uses the same sidenav as the graph view instead of its own
custom navigation, and switching between modes happens through a new
animated graph/app toggle shared by both views.
## Changes
- **What**:
- LinearView hosts the standard `SideToolbar`, filtered to the Assets
and Apps tabs via new `visibleTabIds`/`forceConnected` props. The
bottom-panel and shortcut toggles hide in app mode, and
GraphCanvas/SubgraphBreadcrumb hide their instances so only one toolbar
and toggle render at a time.
- The workflow actions dropdown trigger is now a two-segment graph/app
toggle: the inactive segment switches modes directly, the active one
opens the actions menu as before, with a FLIP animation on switch.
- Each view renders its own toggle, so switching unmounts one and mounts
the other. A `displayLinearMode` ref in canvasStore trails `linearMode`
by two frames so the incoming toggle animates from the old mode instead
of popping in already-switched.
- AppModeToolbar slims down to the toggle plus a "Build an app" button;
the per-icon Assets/Apps/Share buttons are covered by the sidenav.
- The Apps tab gains "Create" header and empty-state actions wired to
`Comfy.NewBlankWorkflow`.
- App-mode feedback moves into `SidebarHelpCenterIcon` as a Typeform
popover with a load-error fallback, replacing `TypeformPopoverButton`
and `LinearFeedback` and simplifying LinearPreview to a single
`OutputHistory`.
- Tests for the toggle, sidebar tabs, help-center fallback, preview, and
view; screenshots regenerated.
## Review Focus
- The two-frame `displayLinearMode` lag — timing-sensitive; rapid
toggling cancels the pending frame chain so the wrong mode never
flashes.
- `SideToolbar` now serves both views: confirm only one instance renders
and the new props behave.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/9ad1eb4f-9370-4975-b3f1-b271482f2bf7
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## ELI-5
Some cloud providers hand you an API key you type in a box; others (like
Google Vertex) hand you a whole JSON file. The provider list already
comes from the server. This teaches each provider to also say *how* its
credential is entered. When a provider says `json_file`, the "Add
Secret" form now shows an **Upload JSON file** button plus a paste box
instead of the single-line password field. Everything else works exactly
as before.
## Summary
Extends the server-driven secret provider schema with optional
`input_type` (`'text' | 'json_file'`) and `label`, and renders a
file-upload + paste textarea for `json_file` providers (e.g. a Vertex
service-account JSON) instead of the password field.
## Changes
- **What**:
- `SecretProvider` ingest schema gains optional `input_type` and `label`
(added to the generated `types.gen.ts` / `zod.gen.ts` — see Review
Focus).
- `listSecretProviders()` now returns the full provider objects (id +
optional `input_type`/`label`) instead of just ids; `availableProviders`
carries them through to the form.
- The credential field switches on the selected provider's `input_type`:
`json_file` → upload button (reads the file into the value) + paste
textarea with light "must be valid JSON" validation; anything else → the
existing password field.
- Provider display label prefers the server `label`, falling back to the
frontend registry label, then the raw id.
- New i18n keys under `secrets.*`.
- **Breaking**: none. Providers that omit `input_type` render exactly as
today (single-line secret). Read/List responses are untouched — the
credential value is still never echoed back.
## Review Focus
- **Judgment call — one provider, credential kind inferred, no toggle.**
Per the design, a single provider is kept and the raw credential value
is sent as-is; the credential kind (AI Studio key vs Vertex SA JSON) is
inferred server-side. The design allowed "inferred from the upload *or*
an explicit toggle"; I chose inference to keep the UI minimal and avoid
introducing a new user-facing gating control. No provider ids are added
or renamed, so the proxy's 1:1 path→provider mapping is unaffected.
- **Generated-file edit (bridge).** The `input_type`/`label` fields were
hand-applied to the generated
`packages/ingest-types/src/{types,zod}.gen.ts`. The upstream
`openapi.yaml` (the generation source) is not in this repo, so this
models the wire contract additively until the backend spec lands the
same optional fields; a future regeneration reproduces them. The fields
are optional, so nothing breaks if the server omits them.
- **No secret ever echoed back.** Only the create request carries the
value; GET/List responses (metadata-only) are unchanged.
Tests: unit coverage added/updated for the provider metadata
passthrough, `selectedInputType`, server label precedence, file loading,
JSON validation, and the form rendering both branches. Targeted vitest
(79 secrets tests), `pnpm typecheck`, eslint, and oxfmt all green.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Adds an accent NEW badge next to top-level header nav items (Products,
Community) on both desktop and mobile.
## Changes
- **What**: New `badge?: 'new'` field on `NavItem`, rendered as an
accent `Badge` in `HeaderMainDesktop.vue` and `HeaderMainMobile.vue`;
adds a `xxs` badge size and drops hardcoded padding/size from the
`accent` variant so it composes with size.
- **Tests**: e2e coverage asserting the badge shows only on
Products/Community, not Company/Pricing.
## Review Focus
- The `accent` badge variant now inherits padding/font-size from `size`
instead of hardcoding them — verify existing `accent` usages still
render correctly.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Follow-up to #12052. The local wheel guard added in FormDropdownMenu was
cancelling native scroll on slow macOS trackpad gestures, leaving image
rows blank in the asset picker until a fast scroll re-pumped samples
through.
## Changes
- **What**: `FormDropdownMenu.onWheel` now only `preventDefault`s on
pinch-zoom (`ctrl/meta + wheel`). The `|deltaX| > |deltaY|` branch of
`isCanvasGestureWheel` is no longer used here.
- **Why**: Slow vertical trackpad scrolls emit small-delta frames with
stray horizontal jitter (e.g. `deltaY=1.2`, `deltaX=1.5`) that satisfied
the horizontal-dominant predicate. Cancelling those frames starved
`VirtualGrid`'s throttled `useScroll`, so `Math.floor(scrollY /
itemHeight)` never advanced and tiles never mounted.
- Horizontal-swipe-to-navigate is still blocked at the page boundary by
`overscroll-behavior: none` on `html, body` (the actual fix from
#12052), so dropping the local horizontal guard does not reintroduce the
back/forward bug.
- The global `isCanvasGestureWheel` is unchanged —
`useCanvasInteractions` still uses it for the canvas wheel handler,
where the semantics are different (forward to canvas pan, not
preventDefault native scroll).
## Review Focus
- Confirm `overscroll-behavior: none` on `html, body` is sufficient on
its own for the horizontal-swipe-to-navigate case inside the teleported
dropdown. (It is — overscroll-behavior is decided per scroll container
at gesture start, independent of where in the page the gesture
originates.)
- Pinch-zoom in the dropdown still preventDefaults correctly.
Tested on macOS trackpad: slow vertical scroll now loads images
progressively; fast scroll unchanged; pinch-zoom in dropdown does not
zoom the page; horizontal swipe in dropdown does not navigate.
Related: this fixes the slow-scroll repro reported in #bug-dump. The
blank-on-reopen class of bug from FE-535 (#11885) is a different issue.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12334-fix-form-dropdown-stop-cancelling-native-scroll-on-slow-trackpad-wheels-3656d73d3650813a987ffa8d6f0eaa9c)
by [Unito](https://www.unito.io)
## Summary
Browsers cap live WebGL contexts per page, so each Load3D node owning
its own renderer broke once enough 3D nodes were added. All 3D views now
share a single offscreen WebGLRenderer: each view renders into the
bottom-left region of the shared drawing buffer and blits the frame into
its own plain 2D canvas, which has no context limit.
- Add sharedWebGLRenderer (refcounted singleton, grow-only buffer) and
RendererView (per-view canvas + per-view renderer state + blit)
- Per-view renderer state (tone mapping, color space, clear color) is
applied before each view renders; HDRIManager and SceneModelManager
write to it instead of the renderer
- OrbitControls/TransformControls bind to the node container explicitly
- ViewHelperManager renders the axes helper itself so it can position
and scale independently of renderer DOM layout
- RecordingManager records from the view canvas; capture renders at
exact size on the shared buffer as before
- On last view release the context is force-lost and webglcontextlost is
dispatched synchronously so context-loss handlers still run
## Screenshots (if applicable)
before
<img width="2155" height="1527" alt="image"
src="https://github.com/user-attachments/assets/35a7ab8a-28dd-41ba-8349-525bd2e97977"
/>
after
<img width="918" height="430" alt="屏幕截图 2026-07-09 135025"
src="https://github.com/user-attachments/assets/67328577-bd82-4494-921a-6d298442d081"
/>
<img width="1636" height="1509" alt="image"
src="https://github.com/user-attachments/assets/a3be9215-7cd0-427c-8264-1fd964d3ace1"
/>
## Summary
Prototype redesign of the embedded 3D viewer controls into a framed
top/bottom-bar chrome. Proof-of-concept for design exploration — not
intended to merge as-is.
## Changes
- **What**: Replaces the floating viewer controls (`Load3DControls`)
with a new `Load3DMenuBar` framed chrome: a black top bar with a
category dropdown (Scene / 3D Model / Camera / Lighting) and the active
category's actions (labels collapse to icons on narrow nodes), plus a
black bottom bar with Record (left) and fit + export (right). Export is
moved out of the menu into the bottom-right button. Adds a "Clay"
material mode that renders meshes with a flat grey material so geometry
is visible without textures.
- **Breaking**: None — `Load3D.vue` swaps the component and hides its
existing right-side button column behind `v-if="false"`; nothing is
deleted.
## Review Focus
- Design prototype: `Load3D.vue` swaps `Load3DControls` →
`Load3DMenuBar` and hides the existing fit/center/expand/record column.
The original components are left untouched.
- The only changes outside the prototype component are for the **Clay**
material mode, which touches shared engine code: `MaterialMode` type
(`interfaces.ts`), the runtime material switch (`SceneModelManager.ts`),
the capability lists (`MeshModelAdapter.ts`, `ModelAdapter.ts`,
`useLoad3d.ts`), and the i18n label.
- Known prototype gaps: **Record** is a visual toggle only (pulsing dot,
not wired to real recording); **fit** reuses the existing
`handleFitToViewer`; **export** reuses the existing format options.
## Screenshots (if applicable)
Prototype walkthrough captured during development (top-bar chrome,
category dropdown, labeled-vs-icon collapse, Clay material, pulsing
Record). Available on request.
---------
Co-authored-by: PabloWiedemann <PabloWiedemann@users.noreply.github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
- loadThread and newChat fired a server-side cancel for the in-flight
turn; the backend recorded it and the reply was lost ("Stopped
streaming..."). Panel unmount did the same. All implicit cancels are
removed; the explicit Stop button is the only cancel left
- switching away parks the live turn (message + event transport) in a
per-thread registry; events keep accumulating silently and cannot
bleed into the displayed chat; switching back re-attaches the turn
live with no lost text, or renders the finished reply from history
- a done landing during the return fetch keeps the parked entry so the
full reply still renders; overlapping or double-clicked history loads
are generation-gated; socket death and panel close settle parked
turns; remount refreshes a surviving thread from history
- draft patches from a backgrounded thread no longer drive the
displayed canvas (thread guard; version heartbeat unchanged)
- transport ignores events after settle; chat history date buckets
follow a reactive clock across midnight; onboarding coach gains
dialog semantics and Escape to dismiss; dead approval-flow fixtures
removed
## Summary
The template picker's search now surfaces the right template for how
people actually type — abbreviations, typos, multi-word intent, and
non-Latin (CJK) titles — and orders results by real popularity instead
of a fuzzy-match score that was being thrown away. Search and ranking
now behave the same here as they do on the workflow hub.
## Changes
**What**
- Searching for the way people phrase things now works: `t2v`, `i2v`,
`cn` expand to their full modality terms, `img2img`/`v2v` expand to
editing (matching how the catalog tags image/video edit templates),
`flux upscale` and `sdxl lora` match across title/model/tag fields
together, prefixes like `vid` match `video`, and typos like `contorlnet`
still find ControlNet. Versioned names tokenize sensibly, so `wan 2.2`
and `wan2.2` both hit, while `2.5` never blurs into `3.5`.
- CJK titles are searchable. Unspaced Han/Hiragana/Katakana runs are
tokenized into character unigrams and bigrams, so a substring a user
types (`放大` inside `图像放大`, or the single trailing `大`) lands on a match.
Korean and other spaced scripts fall to the normal word tokenizer,
unchanged.
- Fuzzy matching is tighter: a term now tolerates edits up to 20% of its
length (down from a flat threshold), so `contorlnet` still finds
ControlNet but `upscale` no longer fuzzy-matches the shorter, unrelated
`scale`. Short (≤3-char) and digit-bearing terms stay exact.
- Results lead with text relevance. Previously the fuzzy match score was
computed and then discarded, and any active sort re-ordered results by
usage — so the best textual match rarely landed on top. Now relevance is
the authoritative order while a query is active, and when two results
match about equally well, the more-used template wins the tie (dampened
so one runaway-popular template can't dominate).
- The ranking is a stable total order. Scores are bucketed before usage
breaks ties, so a cluster of near-equally-relevant results always sorts
the same way — a naive per-pair "within X%" comparison is intransitive
and makes the order depend on internal input order (it can even shuffle
as you type another character).
- "Popular" ranks by raw usage, matching what the hub and the search
index show. It previously blended in a freshness term that pushed newer,
less-used templates above genuinely popular ones.
- The sort dropdown works during search again: it defaults to
"Relevance" but you can switch to Popular/Newest/etc. to re-order the
results, and your browse sort is restored (and never overwritten by a
search-time choice) when you clear the query.
- Alphabetical sort reads correctly: it sorts by the title shown on the
card, trims stray leading whitespace that used to jump templates to the
top, and groups number-prefixed titles after the letters instead of
ahead of them.
- Filter telemetry now reports the sort the user is actually seeing
(relevance while searching) rather than the persisted browse sort, so
analytics reflect the visible ordering.
- Removed the old runtime Fuse-options override path, which is obsolete
under the new engine.
**Breaking**
None. Existing filters (Model / Use Case / Runs On / distribution),
pagination, and persisted sort settings are unchanged; the relevance
mode is search-only and never persisted.
## Review Focus
- The ranking crux is `rankByRelevanceThenUsage` in
`templateSearchConfig.ts`: relevance is primary, usage only re-orders
results in the same score bucket, and bucketing keeps it a stable total
order. That's the one function to review for correctness.
- The CJK tokenizer (`cjkGrams` / `tokenize` in
`templateSearchConfig.ts`): script-matched so only unspaced scripts are
grammed, and a pure-CJK run relies on its grams (no whole-word token).
Splitting by code point is safe here (these scripts are BMP-only; emoji
are excluded by the run regex).
- Deliberately not touched: the "Recommended" sort keeps its curated
blend (usage + editorial rank + freshness) so it stays distinct from
"Popular"; `vram-low-to-high` remains unimplemented exactly as on main.
## Tradeoffs / notes
- Adds `minisearch` (~18 kB gzip). The template selector is where it's
used; accepted for the search-quality gain (a later change could
lazy-load it if bundle size becomes a concern).
- Bucketing means two results just across a bucket boundary don't
tie-break on usage even when their scores are close — the accepted cost
of a transitive, predictable order (this mirrors how the search index
quantizes relevance).
- `img2img` expands to editing (not literal "image to image") because
the catalog labels those templates "Image Edit" — verified against the
real data.
- CJK bigrams roughly double the token count for a pure-CJK title;
negligible at catalog scale (~550 templates, short titles).
## Testing
Behavioral coverage over the real search paths, not the mocks — the
ranking and tokenizer run against actual MiniSearch output; only the
ranking-store math is mocked. Also verified against the full
~550-template catalog end to end (all query types stable, zero
input-order-dependent orderings).
### Behavior matrix (verified on the real catalog)
| Input / action | Now | Previously |
| --- | --- | --- |
| `img2img` | Image-editing templates (Qwen Image Edit, …) | Matched
every "image" template — intent lost |
| `flux upscale` | Flux upscale templates (matches both terms across
fields) | **No results** (single-field fuzzy couldn't span title + tag)
|
| `sdxl lora` | SDXL templates | **No results** |
| `t2v` / `i2v` / `cn` | Expand to text→video / image→video / controlnet
| Only partial slug hits, if any |
| `vid` (prefix) | Matches `video` templates | Unreliable |
| `contorlnet` (typo) | Finds ControlNet | Often dropped by the strict
threshold |
| `upscale` | Matches upscale titles only | Fuzzy-matched the unrelated
substring `scale` |
| `放大` / `大` (CJK) | Matches `图像放大` and other titles containing the run
| No match — CJK titles were unsearchable by substring |
| `wan 2.2` and `wan2.2` | Both match; `2.5` never matches `3.5` | Space
vs no-space degraded the match |
| Near-tied cluster (e.g. `upscale`) | Stable order every time |
Reordered depending on input order (could shuffle as you type) |
| Query active, "Popular" selected | Best textual match still leads;
usage breaks near-ties | Sort re-ordered by usage, burying the best
match |
| Change sort while searching | Re-orders the search results; relevance
is the default | Sort was locked; dropdown had no effect |
| Clear the search | Restores the browse sort you had before | — |
| "Popular" sort | Orders by raw usage (matches hub / index) | Freshness
blend pushed newer low-usage templates up |
| A–Z sort | Letters first (`ACE…`), number-prefixed titles last
(`3x3…`, `360…`); leading whitespace ignored | Leading-space titles
jumped to the top; numbers sorted before letters |
### Unit tests (81 total, all passing)
- `templateSearchConfig.test.ts` (34) — tokenizer identifier/version
splits, CJK unigram/bigram gramming (and Korean left as a spaced word),
per-term fuzziness (`upscale` ≠ `scale`), abbreviation expansion (incl.
`img2img`→edit intent), prefix + typo matching, AND-then-OR,
literal-before-expansion ordering, relevance>tag>description ranking,
and `rankByRelevanceThenUsage` giving a stable order on an intransitive
cluster.
- `useTemplateFiltering.test.ts` (35) — the `img2img` / `flux upscale` /
`sdxl lora` regressions, relevance-default-on-search,
override-sort-while-searching, browse-sort restore on clear, ephemeral
mid-search sort, telemetry reporting the visible sort, Runs-On filter,
empty-result guard, filters preserving relevance order, and alphabetical
trimming + numbers-after-letters.
- `templateRankingStore.test.ts` (12) — freshness and default-score
(recommended) math.
Gate: `pnpm typecheck`, `pnpm lint`, `pnpm knip` clean.
## Screen Recording (if applicable)
https://github.com/user-attachments/assets/6748a3f7-e69d-44ac-826c-71990c8dce90
Deployed builds keep reading the token from the backend-injected __CONFIG__
(precedence unchanged); the env fallback only enables event verification from
local dev servers, which otherwise have no key and silently queue forever.
Per the per-file consolidation audit (temp/plans/pr13472-file-consolidation-audit.md):
safetyTypes -> ConflictDialog (SFC type export, ActiveTabStrip pattern),
agentTranscript + useDraftCanvasApply -> AgentPanelRoot, ThinkingStatus ->
AgentMessage, SuggestedPromptChip -> EmptyState, SelectionTagChip -> Composer,
SessionBar -> AgentPanel. All moves byte-identical; net -9 files.
agentTranscript's test is replaced by a stronger end-to-end copy test
asserting the exact clipboard markdown. useDraftCanvasApply's tests were
retired as redundant rather than merged: patch-to-canvas firing is covered
end-to-end by the draft binding suite, rejection semantics are owned by
agentDraftStore.test.ts, and the stop handle was unused API removed by the
fold.
- autosaveAppliedDraft only reset the change tracker and isModified,
but the tab dot renders whenever a workflow is not persisted, so
agent-applied drafts always showed unsaved and lived only in memory
- save through workflowService instead: temporary tabs use a silent
saveWorkflowAs with a collision-safe filename (no prompt or
overwrite dialog), persisted tabs re-save silently
- rebind workflow_id to the tab current path in finally so service
renames (collision bump, app extension mismatch) and failed saves
never strand the binding on a dead path
- known limit: host saveWorkflow drift handling can rarely toast or
confirm on persisted tabs; reusing the canonical save path there
is intentional
- tests: failure isolation keeps the applied draft and surfaces no
error, an app-mode collision bumps the filename and rebinds, a
save that fails after rename still rebinds; binding tests now
stub a valid empty threads page
The backend now seeds the thread's draft from the graph the client
sends each turn. Align to its shape and rules:
- The turn POST carries draft: { content: <serialize() save format>,
version: <last seen draft version | null> } (was the inert
workflow/graph/last_seen_version shape).
- Snapshots gate on canvas-non-empty, not workflow_id, so templates,
unsaved tabs, and new tabs seed correctly; skipping an unchanged
canvas stays as the server-blessed optimization.
- A 409 {error, version} means the agent's draft advanced: adopt the
returned version and retry the send once.
The 2MB cap guarded a server limit that does not exist yet and silently
skipped uploads for large graphs, which are normal on a video/image
platform. The server owns its own limits.
## Symptom
When the auto-backport workflow (`.github/workflows/pr-backport.yaml`)
hits a cherry-pick **conflict**, it is supposed to comment on the
original PR telling the author to backport manually. On PR #13359
(backport to `cloud/1.45`, cherry-pick of `d6c582c39` conflicting on
`useSubscriptionDialog.test.ts`) **no comment was posted** — the author
(@huntcsg) got no notification and had to find the failure by digging
into Actions logs. See run
[28616756256](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28616756256/job/84862400420).
## Root cause
The "Comment on failures" step actually ran and reached the `conflicts`
branch — the failure reason *was* populated and the `if:` condition
*was* met. The real failure is at the last line of the loop, under the
step's default `bash -e` shell:
```yaml
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
```
The job log ends with:
```
GraphQL: Unable to create comment because issue is locked (addComment)
##[error]Process completed with exit code 1.
```
PR #13359 is `locked: true`, so `gh pr comment` returns non-zero.
Because the call was unguarded under `set -e`, the step aborted on the
spot: the comment was lost and — critically for the general case — any
remaining failed targets in the loop would also be skipped. The step is
then marked failed with no actionable output on the PR.
(Related PR #13167 "attempt each backport target branch independently"
is still open/unmerged; this is a residual gap in the failure-comment
path.)
## Fix
Wrap every `gh pr comment` call in a `post_comment` helper. On failure
it emits a `::warning::` naming the target, the reason, and the
manual-backport branch (`backport-<pr>-to-<target>`) instead of
aborting, so the loop always attempts a comment for each failed target
and surfaces a clear log message when GitHub refuses (e.g. locked
issue). The conflict comment body now also states the manual backport
branch explicitly.
- YAML: `python3 -c 'import yaml; yaml.safe_load(...)'` passes.
- `actionlint`: no new warnings in the changed step (the 2 pre-existing
`SC2016` notes on the intentional single-quoted `envsubst` var lists are
unchanged).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Minimaps unmounted cleanup blindly restored the callbacks that are
originally captured, even if other systems have chained their own
callbacks onto this, breaking other parts of the system (e.g. vue node
graph manager).
Recreation:
1. Ensure minimap open
2. Enter subgraph
3. Exit subgraph
4. Close minimap
5. Try adding a node/unpackign subgraph/etc <--- broken
## Changes
- **What**:
- only replace callbacks that we own
- else function becomes no-op
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12380-fix-dont-remove-unowned-callbacks-when-cleaning-hooks-on-unmount-3666d73d3650817cbfe0d98ab98528b8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Make CLA more robust by including commit authors in the allowlist even
if they have no GitHub account. This to ensure only PR authors are
required to sign.
## Changes
- **What**: `cla.yaml`
The module map caches a failed module resolution, so re-importing the
same specifier rejects from cache without refetching; the retry could
never succeed. Stale-chunk failures are already handled app-wide by the
vite:preloadError listener in App.vue.
The client hardcoded person_profiles: 'identified_only' AFTER the
posthog_config spread, silently overriding the server's explicit
'always' (live on /api/features). Under identified_only every event
captured before identify() resolves is a person-less anonymous event
that never attaches to the user, so early-session events (entry click,
panel opened, first message) vanish from person-scoped PostHog views.
The hardcode is now a default the server can override; before_send
stays locked (PII stripping). Reverses the person_profiles half of the
pin added in #12479, test updated accordingly.
Also retries the posthog-js dynamic import once: a stale deploy can
404 the chunk, which previously disabled telemetry for the entire
session after a single console.error.
Merges the reviewed trim (-1,189 net lines, 19 fewer files, shiki
dropped, four host regressions fixed, zero functionality loss;
validated by a full review pipeline) into the PR branch as a single
revertible merge. Conflict resolutions favor the amended stable side
where it superseded the trim: raw-serialization autosave baseline
(schema-normalized baselines re-flip isModified via graphEqual),
dedupe-gated mention telemetry, the richer WorkflowTabs CTA tests, and
the validate-once assertion; the trim's ws event fake wins in the Root
tests.
autosaveAppliedDraft re-baselines with the RAW canvas serialization: a
schema-validated baseline diffs against raw captures under strict
graphEqual and re-raises the conflict dialog on an unedited canvas. A
test pins validateComfyWorkflow to its single loadDraft call so the
baseline can never route through the schema.
Coverage hardening from review: entry-click resulting_state both ways,
close-button click attribution through the real store, message_sent
payloads (attachments and node tags), mention-pick positive plus
stages-nothing negative, null open_duration_ms, flag_disabled pin.
- ToolCallGroup: streaming auto-open, same-name merge, failure reopen,
and the AgentMessage adjacent-merge branch with pluralized header.
- WorkflowTabs: the Ask Comfy Agent CTA renders when enabled and
toggles the panel (mock rewired ref-backed for storeToRefs).
- useAgentSession: non-object and foreign host frames drop silently
mid-turn.
- MarkdownStream: fence label picks the first info-string word.
The trim deleted the collapsible keyframes as duplicates of
tw-animate-css, but that package defines no collapsible-* keys, so the
tool-disclosure animation silently no-opd. Restored under agent-*
names: the globally-imported sheet can no longer retarget the host's
animate-collapsible-* utilities (undefined on main) in sharing dialogs.
Adds the three review-flagged test gaps: indented code stays prose,
bare fences label as text, and the oversize-attachment message pins
its i18n interpolation.
Adds the V0 FE metric set through the existing telemetry layer (typed
events, optional provider methods, cloud PostHog provider):
- app:agent_panel_opened / app:agent_panel_closed with source
(topbar_button | close_button | flag_disabled) and open_duration_ms,
so open-vs-closed share per session is computable
- app:agent_entry_button_clicked (resulting_state) and
app:agent_close_button_clicked for the raw clicks
- app:agent_message_sent (attachment_count, node_tag_count),
app:agent_node_tagged (mention_picker), app:agent_attach_button_clicked
- app:agent_workflow_applied (workflow_id, new_tab | existing_tab) for
agent-created vs agent-edited workflow attribution
- app:agent_message_feedback now carries workflow_id (PM-98)
The panel store owns open/close state, so it emits the state events with
an idempotency guard (the flag gate re-syncs close on every evaluation).
The entry/close click events are kept alongside the state events
deliberately: the V0 metrics checklist names them as PM-queryable
trackers, though the state events' sources make them derivable.
Also hardens autosaveAppliedDraft: the tracker re-baselines with the RAW
canvas serialization. A schema-validated baseline would diff against raw
captures under strict graphEqual and re-raise the conflict dialog on an
unedited canvas; a test pins validateComfyWorkflow to its single
loadDraft call so the baseline can never route through the schema.
- 17 locale keys with zero usages removed from en (other locales are
lobe-i18n generated and never carried them); error-catalog and
design-locked keys kept.
- useAttachment strings go through i18n; the 20MB label derives from
the named constant (max_upload_size flag exists but has no production
consumer, so the constant stays).
- A failed thread-list fetch now raises the shared error modal instead
of silently showing empty history, with a regression test.
- Safety UI that nothing drives (ApprovalCard, LockBanner, RevertButton
and their AgentPanel plumbing); ConflictDialog stays.
- Dead message-part clusters: file/asset rendering and reasoning
disclosure that no transport path constructs; the parts union and
AgentMessage grouping shrink to text/tool/notice.
- Unwired composer pieces (AssetTray, SelectionActionChips,
useCloudAssets) with zero non-test importers.
- Dead exports (createThread, history.upsert, useAgentFeatureGate fn,
hasDraft, onboarding multi-step machinery) - each re-verified for
zero production consumers on the current head; parseAgentWsEvent /
isAgentEvent kept, they are live imports since the event rework.
- Write-only token pipeline, info/warning notice machinery, and the
transport's no-op finalize loop removed; finalize/abort merged into
one settle().
- The agent REST client rides api.fetchApi (Firebase init-wait, auth
headers, 401 remint, Comfy-User) instead of a hand-rolled fetch with
workspace-token-only auth; zod parsing and AgentApiError stay.
- Agent-local Button/Textarea forks replaced by the shared UI kit with
variant mappings verified pixel-equivalent through tailwind-merge;
dialogs inline reka primitives (the shared kit's entrance animations
and sizing cannot be neutralized). ScrollArea/CollapsibleContent
wrappers inlined per the sibling idiom.
- WorkflowTabs CTA uses the shared Button.
- agentTheme.css no longer redefines tw-animate-css collapsible
keyframes (they globally retargeted three sharing-dialog consumers).
- CodeBlock keeps the framed header + copy but renders plain monospace;
the shiki dependency and catalog entry are removed. MarkdownStream
tokenizes fences via marked.lexer, fixing the 4-backtick gap.
## Summary
Keep the onboarding survey's Back/Submit footer visible on small screens
by scrolling only the question area instead of the whole survey.
## Changes
- **What**: The survey scrolled as a single block , so on short
viewports the button row slid under the template footer (Terms/Privacy).
Now the outer is bounded to its slot and the question wrapper in
`DynamicSurveyForm` scrolls internally with a responsive cap , so option
lists scroll while the footer buttons stay pinned. The step height
animation is unchanged.
## Screen Recording
https://github.com/user-attachments/assets/6d7f6d50-59b8-4dc0-b20e-8f4ca08167c6
- MarkdownStream renders through the shared markdownRendererUtil; the
agent copy (and its module-load DOMPurify hook that gave every host
anchor target=_blank) is deleted.
- The agent event source is now a thin adapter over the api singleton's
typed custom-event dispatch: no second socket follower, no re-parse of
every /ws frame, no "Unknown message type" throw noise. Zod payload
validation and the malformed-event notice stay.
- AgentPanelRoot loads via defineAsyncComponent, keeping the flag-gated
subtree out of the GraphCanvas chunk.
- Prompt-error cards show details only for the agent's own error types;
host cards restored to pre-PR behavior, both pinned by tests.
## What
Browser half of GTM-93 macOS web→desktop identity stitching: when the
desktop app opens the system browser at cloud login with
`?desktop_login_code=dlc_…`, the frontend redeems that code against the
cloud backend once a Firebase session exists — after **explicit user
approval**.
Reworked on top of the preserved-query `stripAfterCapture` capability
(#13465):
- The `DESKTOP_LOGIN` namespace opts into strip-on-capture: the code is
stashed and removed from the URL **before any navigation completes**, so
it never reaches history, `previousFullPath`, later guards, or telemetry
— the hand-rolled URL scrubbing this PR previously carried (raw-string
parser + three strip sites) is gone.
- `desktopLoginRedemption.ts` is a plain module with a single export,
`installDesktopLoginRedemption(router)`, installed once in `router.ts`'s
cloud block (replaces six per-view/composable trigger sites). Redemption
reads the code only from the stash: per-code state (approval + 2-attempt
transient budget, so a second code gets its own approval and budget),
approval dialog, `POST /api/auth/desktop-login-codes/redeem` with the
raw Firebase ID token (backend route is Firebase-JWT-only), 10s fetch
timeout.
- Triggers: `router.afterEach` (the cloud auth guard settles Firebase
init before navigations complete) plus a lazy watcher on
`authStore.currentUser` for sessions that appear without a navigation
(OAuth-resume error branch, dialog sign-in). One bounded in-page retry
(5s) guarantees an approved sign-in always ends in a success or failure
toast.
- Terminal rejections (400/403/404/409/410) drop the code with an error
toast; transient failures (401/5xx/timeout/network) retry once; budget
exhaustion now surfaces a failure toast instead of dying silently.
## Why
Windows stitches web→desktop at download time via installer stamping;
macOS DMGs can't be stamped, so we stitch at login. The browser is where
both halves meet: the existing `posthog.identify(uid)` merges the web
anon person into the Firebase uid, and the backend emits
`comfy.cloud.identity.login_attributed` (uid ↔ installation_id) at
redeem. The desktop app polls the backend and receives a one-time custom
token — no auth material posted to a desktop loopback server (the
concern that stalled #12983, which this supersedes).
## Security
- **Approval dialog before redeem** — redemption mints the desktop a
sign-in token for *your* account, so a lured click must not be enough
(device-code phishing mitigation). Cancel clears the stash and does
nothing.
- Only the opaque single-use code ever appears in a URL; the tracker
strips it on first sight, pre-navigation, and it is never logged.
## Testing
Vitest, driven through a real router (createRouter/createMemoryHistory,
no vue-router mocks) and the real preserved-query manager: capture/stash
lifecycle, approval gate (no fetch before approve; decline/dismiss
clears; per-code approval), Bearer/body shape, terminal-vs-transient
statuses, timeout abort, bounded in-page retry + failure toast on budget
exhaustion, per-code regressions (second code after
success/decline/exhaustion redeems independently), auth-watcher trigger
(session appearing without navigation), unauthenticated no-op, trigger
coalescing. Typecheck/lint/format clean.
Types are hand-written with a `TODO(@comfyorg/ingest-types)` — the
generated types land automatically once the cloud PR merges and the
type-gen workflow runs.
## Landing order
1. Cloud backend: https://github.com/Comfy-Org/cloud/pull/4736 (until it
ships, redemption never triggers — this PR is inert)
2. #13465 preserved-query strip-on-capture (base of this PR)
3. #13466 global-prompt FIFO queue (runtime dependency: the approval
confirm must settle even if another prompt is open)
4. **This PR**
5. Desktop (activates the flow):
https://github.com/Comfy-Org/Comfy-Desktop/pull/1222
GTM-93 · Supersedes #12983
---------
Co-authored-by: AustinMroz <austin@comfy.org>
Adds the V0 FE metric set through the existing telemetry layer (typed
events, optional provider methods, cloud PostHog provider):
- app:agent_panel_opened / app:agent_panel_closed with source
(topbar_button | close_button | flag_disabled) and open_duration_ms,
so open-vs-closed share per session is computable
- app:agent_entry_button_clicked (resulting_state) and
app:agent_close_button_clicked for the raw clicks
- app:agent_message_sent (attachment_count, node_tag_count),
app:agent_node_tagged (mention_picker), app:agent_attach_button_clicked
- app:agent_workflow_applied (workflow_id, new_tab | existing_tab) for
agent-created vs agent-edited workflow attribution
- app:agent_message_feedback now carries workflow_id (PM-98)
The panel store owns open/close state, so it emits the state events with
an idempotency guard (the flag gate re-syncs close on every evaluation).
Draft applies now re-baseline the bound tab's change tracker to the canvas
as loaded and clear isModified, on both the in-place and minted-new-tab
paths. A minted tab's stored baseline carries an ensureWorkflowId id the
canvas never adopts, so the first capture flipped isModified and every
following draft_patch raised the conflict dialog with zero user edits.
Manual edits still re-arm the dialog.
The thumbs up/down + copy row under a completed agent message no longer
hides until hover (V0: always display).
Rename the WorkflowUpload fields so the wire contract reads plainly:
content -> graph (the prompt is already the top-level content field), and
base_version -> last_seen_version (the draft version the client last
received; draft_version would have collided with the existing WS event
type of that name).
Tags send bare node_ids again; they resolve against the full graph the
same POST already uploads (workflow.content) once the server seeds the
draft from it. No client-side stopgap.
REST failures (e.g. Bad Gateway on hydrate), malformed events, and
upload failures rendered as bespoke toasts floating over the docked
panel. They now share the one host error modal already used for
draft-apply failures; warnings/info stay transient toasts.
postMessage now carries the serialized active graph when it changed
since the last upload (2MB cap, re-upload after failed sends or thread
switches), per the agreed cloud contract; current servers ignore the
field. The ack binds the uploaded tab to the thread's workflow id, so
agent patches apply in place on the user's own tab. Empty drafts park
everywhere, and the conflict dialog still guards modified tabs.
The server-side draft never mirrors the live canvas (it starts empty
even for saved workflows), so @-tagged node_ids resolved to nothing and
the agent replied that the canvas was empty. Each tagged node now rides
the POST with its serialized definition from the graph the canvas is
showing (root or an open subgraph), plus a context note that claims
inline definitions only for tags that resolved, names the gap when some
did not, and nudges a save when the tab has no server-side workflow.
- The composer @ button now opens a dropup listing the active graph's
nodes (title + id); picking one stages it as a selection tag.
- An unbound draft patch with no nodes no longer opens a blank tab;
the server-minted empty draft parks until a patch carries content.
- Selections sent from a tab with no server-side workflow carry a
context note so the agent asks for a save instead of guessing.
Sent node tags now persist as small pills on the user message (same
rationale as attachments: proof they were sent), with the node ids still
riding the POST selection. The tag titles travel through sendMessage
alongside attachments, replacing the session's selection dep.
The active tab name moves out of its own row into the panel header next
to the ALPHA badge (truncating), reclaiming a strip of vertical space.
A bare {token} fails WorkspaceTokenResponseSchema, sending workspace auth
into its recovery/retry loop whose error toasts stack over the docked
panel and swallow the suggested-prompt click (the intermittent cloud
failure). The mock now carries expires_at/workspace/role/permissions, so
the app authenticates cleanly and nothing toasts.
Boot + panel open + a full streamed turn can exceed the cloud project's
15s budget on a loaded CI runner (observed: test-timeout at the chip
click with everything before it slow but passing). Same precedent as
versionMismatchWarnings.spec.ts.
Review pipeline hardening: attachment object URLs now revoke on
conversation reset/hydrate (sent previews no longer outlive their
conversation); the conflict dialog's dropdown trigger reuses Button
instead of hand-copied variant classes; the dead useCanvasContext
composable is removed (ActiveTab lives with the strip).
New regression tests: draft-patch coalescing under a slow canvas apply,
the full preview-revoke lifecycle (dismiss, upload-fail, reset, hydrate,
never on submit), two-turn attachment keying, tab-strip reactivity,
dialog X-close semantics, uploading send-block, UserMessage render arms,
and loadThread dropping the prior draft binding.
Compact card with the design's copy ("Agent changes are ready") and a
horizontal action row: Cancel/close defer the decision and the next turn
re-asks; "Keep my changes" retires the pending draft version so only a
newer agent edit asks again; "Accept agent changes" is the primary with
"Open in new tab" in its attached dropdown.
Sent images now stay visible as a thumbnail grid on the user message (they
previously vanished on send, reading as lost). Attachment chips stage the
moment a file is picked, spin while uploading, and block the send until the
ref exists server-side; failures remove the chip and toast. Dismissed chips
release their local preview buffers.
Draft patches now route to the canvas tab bound to their workflow_id:
in place when the tab is active (was: a new tab per patch), lazily on
refocus when backgrounded, and through the merge-conflict dialog when
the user edited the tab (keep mine / let agent continue / open new tab,
per B16). An unbound workflow opens and binds its own tab on first patch.
Every message POST carries the active tab's workflow_id (bound id, else
the saved workflow's uuid; a 403 on a speculative id retries once
without). Selected canvas nodes stage removable @-chips above the
composer and ride the turn as selection.node_ids, per B7.
Unmocked, both reject in CI and their sticky toasts stack over the docked
panel, swallowing chip and Send clicks. Reuses the shared mockBilling helper
and the shared jsonRoute in place of a local duplicate.
The mocks lived in a sibling fixture with no ordering guarantee against
comfyPage, so in CI the app booted against the real /api/features (no
PostHog token) and the flag gate kept the panel button hidden. Override
the page fixture instead, the same pattern as workspaceSwitcher.spec.ts.
A failed draft-to-canvas apply is a workflow error, so it now uses the
existing production error surface instead of a bespoke toast: the panel
writes lastPromptError (type agent_draft_apply_failed, details = the
zod/load failure) and opens the error overlay, whose View details opens
the Errors tab. Once per failure streak, reset when a draft applies; a
loadGraphData rejection surfaces the same way. Chat/session errors
(failed send, malformed event, cancel, history) keep their existing
inline/notice paths.
- promptErrorResolver: register agent_draft_apply_failed; catalog copy
under errorCatalog.promptErrors
- useErrorGroups: pass prompt-error details through to the card item so
View details shows the raw failure (TabErrors expectation updated)
- agentPanel.spec: the bare main.json import crashed Playwright's Node
loader at collection time, failing every project; add the
'with { type: json }' attribute (same as WorkflowTabs.test)
- WorkflowTabs.test: mock the agent panel store like the component's
other stores; its new useAgentPanelStore() call threw no-active-pinia
Comment-only sweep of the subtree (zero executable lines changed,
verified by diff filter): 424 -> 107 comment lines across 18 files.
Deleted every restatement of the code, symbol-paraphrasing JSDoc,
design-doc narration, and story references. Survivors are 1-2 line
facts the code cannot express: wire-contract behavior (409 = settled,
usage null on cancel, title empty until server names the thread),
prevented bug classes (send/ack race, stale-highlight race, IME,
drop navigation, id collisions, reactivity traps), and safety
invariants (fail-closed flag gate, cancel-on-unmount billing).
- CodeBlock: cancel superseded async highlights via watch onCleanup (a
stale resolution could overwrite a newer one during streaming) and
sanitize shiki output with DOMPurify before v-html, matching the
markdown pipeline
- AgentPanelRoot: unsubscribe the event source only after the unmount
cancel settles (stopTurn().finally(stop))
- ConversationView: replace the deep entries watch (O(all entries) per
streamed token) with a signal over the last entry only
- ApprovalCard: guard Approve/Deny against double-click double-emit
- useAttachment: pending spans the whole batch instead of flickering
per file; add a mixed-batch test (fail one, keep the rest)
- Lightbox: muted + playsinline so video autoplay works cross-browser
- agentApiSchema: zAgentMessage tolerates additive fields like its
sibling wire objects
- Composer.test: type the mount helper via ComponentProps
- README: fix the stale opening (right dock, not a sidebar tab)
Adversarially verified audit of the agent subtree against the repo rules:
- MarkdownStream, UserMessage: compress comments that restated the
Tailwind classes below them; keep only intent and provenance
- useCloudAssets: replace the hand-rolled Set dedup loop with
es-toolkit uniqBy (same keep-first-by-id semantics)
- agentPanel.test: move the mutable mock state the vi.mock factories
close over into vi.hoisted, drop the agentClose alias for the
store's own vi.fn
"The agent edits your graph as a draft." -> "The agent edits your
workflow as a draft." — user-facing surfaces say workflow; graph is
internal vocabulary. Still a single line at the 420px panel width.
GET /api/agent/threads returns {threads, pagination}, not a bare array,
so the speculative z.array schema written before the endpoint existed
failed at the root and Chat History rendered empty against a live 200.
Validated against a real response:
- zAgentThreads: {threads: [...]} envelope; rows are {id, title,
preview, last_message_at, updated_at, created_at, ...} with title
"" until the server names the thread
- listThreads unwraps the envelope and returns the rows
- toChatSession drops the guessed field spellings; an unnamed thread
titles its row from preview (the first prompt), then untitledChat
- refreshHistory logs a fetch/parse failure instead of swallowing it
A draft_patch whose content fails the host workflow schema (e.g. a
graph missing its required version field) was dropped with only a
console.warn: the agent narrated a finished graph while the canvas
stayed stale and the user saw no error. Raise an error toast on
rejection (new agent.draftApplyFailed key), once per failure streak
rather than per patch, reset when a draft applies.
The history screen's "<- Chat history" control is the current screen
title with a back arrow, but clicking it returns to the chat, so the
label reads like a destination. Add a "Back to chat" hover tooltip
(new agent.backToChat key), mirroring the session bar's "Show chat
history" tooltip.
- Session-bar default title "New chat" -> "Untitled"; it still renames
to the first prompt on submit. agent.untitledChat is consolidated to
"Untitled" so the titleless fallback reads the same everywhere.
- Session-bar hover tooltip "Chat history" -> "Show chat history" (new
agent.showChatHistory key) so the pill names its action; the
new-chat action stays on the header icon.
Matches the DES-455 chat-history flow (Figma node 2442-9503).
CodeBlock and MessageFeedback drifted to text-agent-fg-muted while
every sibling secondary-text run uses text-agent-fg-subtle, the alias
agentTheme.css documents for secondary text. Both aliases resolve to
muted-foreground today, so this is a visual no-op, but it keeps the
panel on a single token if the aliases ever diverge.
- Session-bar fallback title "New Chat" -> "New chat"
- Greeting block: tile-to-text gap-6 -> gap-4
- Greeting wrapper: tracking-tight, snug line-height at both text
sizes, gap-2 between the two lines, max-w-sm
A failed send raised both a host toast (pushError) and an inline
conversation notice (recordFailedSend), rendering one error twice; the
top-right toast also overlapped the right-docked panel. Session notices
are reserved for errors with no inline row, so drop the toast on the
send path and keep the inline row. Test (c) now asserts a send failure
raises no session notice.
- ThinkingStatus: brain icon and a shimmer on the "Thinking…" label
(new agent-shimmer keyframes; static muted fallback under
prefers-reduced-motion and .disable-animations)
- CodeBlock: bordered chrome; header gains a file-code icon, the
language, and a bordered copy button; mono body
- MessageFeedback: gap-0.5 row, size-6 buttons, size-3.5 icons
- Composer: send button shows a loader-circle spinner while the turn
is thinking, via a new submitting prop threaded
AgentPanelRoot -> AgentPanel -> Composer
Kishore shipped the list-threads endpoint, so Chat History is no longer a
single local 'current' row.
- New listThreads() on the REST client + a tolerant zAgentThreads schema
(every field optional + passthrough): the endpoint shipped after the openapi
extract, so the client normalizes id / title / timestamp across the likely
spellings and a shape drift degrades to a best-effort row rather than a hard
zod failure that would blank the list.
- useAgentSession gains listThreads() and loadThread(id): loadThread cancels
any in-flight turn, adopts + persists the thread id, and hydrates its
transcript (reusing the B17 resume path; a stale id 404s and is forgotten).
- AgentPanelRoot fetches the list on mount and each time history opens
(best-effort: a failed fetch keeps the last-known list), maps threads to
ChatSession rows, tracks the active row off the live threadId, and loads a
picked chat via loadThread. Removed the old local 'current row' mirror.
- History store gains replaceAll() for the authoritative server list.
Verified live: the panel calls GET /api/agent/threads and, because the
endpoint 404s on the pr-4432 preview env (Kishore's build not deployed there
yet), the history screen shows 'No conversations yet' with no crash — the
graceful-degradation path. Real rows will populate on an env with the
endpoint; the field mapping should be reconfirmed against the live shape.
Typecheck/knip clean; agent tests 180/180 (added loadThread + listThreads +
server-history-population coverage).
Three reported bugs:
- History did nothing: it was a reka Dialog teleported to <body> that opened
behind the docked panel's z-999 stacking context, so it never appeared.
Replace it with an in-panel Chat History screen (new ChatHistoryScreen)
that the session bar swaps in over the conversation, with a back arrow;
picking a chat or starting a new one returns. No teleport, no stacking
fight. Deleted ChatHistoryDrawer + its DropdownMenu* deps.
- No way to make a new chat: the header new-chat button opened the
out-of-V0-scope 'starting point' onboarding modal instead of resetting.
It now just clears to the empty state; deleted StartingPointModal.
- Textarea border wrong: the panel-scoped Preflight reset in agentTheme.css
covered <button> but not <textarea>/<input>/<select>, so the textarea kept
its UA border/font inside the composer's own border (double border). Extend
the reset to those elements (border 0, transparent bg, inherit font).
Verified live: session bar -> in-panel history -> back; new-chat -> empty
state; textarea border now 0 (single border on the composer container).
Typecheck/knip clean; agent tests 177/177.
Computed-style diff of our conversation vs the design reference (prototype);
re-probed live after the fix for an exact match on the user bubble.
- User message: right-aligned pill, no avatar (dropped the Avatar and the
name/userName props that fed it) — w-fit rounded-lg bg-agent-surface-raised
px-4 py-3 text-xs (was rounded-agent bg-agent-pill max-w-sm px-3 py-2
text-sm with an avatar; bubble was 14px/#303036/radius12, now
12px/rgb(38,39,41)/radius8/py-3 px-4 to match).
- Markdown prose: h1 text-2xl/600, h2 text-base/600, p 14/1.625, decimal/disc
lists, accent underlined links, 3px muted blockquote, bordered
secondary-surface tables (were entirely missing), bordered inline code.
- Tool-call summary row: wrench + 'Ran N…' summary + right chevron on a
borderless h-8 rounded-md muted row (was a bordered card with a left
chevron); expanded steps render as a plain gap list.
- Conversation column: mx-auto max-w-[640px] p-4.
Verified live on localhost against the prototype. Typecheck clean; agent
tests 177/177.
Iterative style pass against the design prototype (visual reference only; all
changes are tweaks to our own components):
- Dock: the panel is now a full-viewport-height right column beside the tab
bar (was a splitter percentage panel below it), 420px default <-> 960px
maximized with a drag handle on its left edge; 8px inset wrapper with a
rounded interface-stroke border.
- Entry: the 'Ask Comfy Agent' button moves from the actionbar row into the
workflow tab bar (plum-bordered ink pill, yellow Comfy-C, open state
highlights); still flag-gated fail-closed. The extension is now gate-only.
- Header: h-12/px-4, regular-weight title, neutral ALPHA pill (no
semibold/tracking), icons message-circle-plus + maximize-2/minimize-2 +
x with tooltips.
- Session bar: compact h-6 rounded-sm pill (align-justify icon, truncated
title, no chevron) instead of a full-width bordered row.
- Empty state: ink-700 tile with plum-600 border and size-6 mark; greeting
scales text-base -> text-2xl via container query; suggested prompts are
rounded-full pill chips (size-3 icons) that wrap centered at >=460px;
4th prompt icon corrected to message-circle-warning.
- Composer: rounded-2xl container owning all padding, focus-within border
brighten, min-h-20 textarea (px-4 py-3), toolbar px-3 py-2, Auto gets its
chevron, send is rounded-xl light-on-dark with opacity-50 disabled state;
placeholder is now 'Ask Comfy Agent…'.
- Footer: p-4 with a mx-auto max-w-[640px] column and my-0 caption.
- Tokens: agent-* aliases retuned to the reference's host tokens
(base-background surface, secondary-background raised/hover,
muted-foreground for all secondary text, component-node-border dividers).
Running mismatch list: temp/plans/fe-1187-figma-acceptance.md
The thread id lived only in the in-memory store, so a reload always opened a
blank panel and the next send even split the transcript onto a new server
thread. Per the backend contract, GET /api/agent/threads/{id}/messages is the
page-load history endpoint (seq ascending; 404 = thread not found).
- Persist the thread id (localStorage) when a send's ack adopts it; clear it
on new chat.
- On session start with an empty store, restore the persisted thread and
hydrate its transcript through the new conversationStore.hydrate(): rows
pair by turn_id in seq order and arrive settled; a turn whose assistant row
is missing still renders its user prompt. A 404 forgets the stale id
silently; reopening the panel within the same page session leaves the live
conversation untouched.
- Tests: hydrate-on-start, stale-404 forget, persist-on-send/clear-on-new-
chat, no-clobber-on-reopen; the harness clears localStorage between tests.
The Figma docks the agent on the right of the canvas (pushing the canvas
left) and opens it from the top-bar "Ask Comfy Agent" button — the left rail
keeps Assets/Nodes/etc. and has no agent icon. It was previously a left
sidebar tab.
- New agentPanelStore holds the panel's { enabled, isOpen }. agentPanel.ts no
longer registers a sidebar tab; the top-bar button toggles isOpen, and the
PostHog flag gate drives enabled (fail-closed: flag off hides the button and
closes the panel).
- GraphCanvas renders AgentPanelRoot in the host right-side-panel slot when the
panel is enabled + open (taking precedence over the node-properties panel),
and the splitter shows that offside panel while the agent is open, so the
canvas shrinks to the left. The panel's close button closes the store.
- Update the unit + e2e specs to open via the top-bar button.
Verified live: opens/closes on the right, canvas pushes left, left rail
intact, no agent icon in the rail. Typecheck + unit tests pass.
Against the finalized "In-App Agent V0 - Design & Requirements" doc and its
Figma frames (fileKey G7ZjPEAFJB8cqb7halGEfa):
- Empty state: the Comfy mark sits on a dark rounded tile; both greeting
lines are bold; all five suggested prompts are pinned just above the
composer (mark + greeting center above them) instead of overflowing, each
a light row with its Figma leading icon (lightbulb, list, search,
message-circle-question, workflow).
- Composer bottom row (B6): add the @ (mention) button beside the paperclip,
add the "Auto" model label, and make the send button white with a dark
up-arrow when active / grey when empty (was always the blue accent).
- Header (B3): ALPHA is a neutral outline badge (was blue-tinted), new chat
uses the speech-bubble icon, and the history clock is removed.
- Add the session bar (B4): a "New Chat" row under the header that opens
Chat History (the history entry point the header clock used to be).
- Rename the sidebar tab label from "AI Agent" to "Comfy Agent" to match the
panel title and branding.
Verified live against the Figma frames.
Tailwind Preflight is disabled host-wide (PrimeVue + litegraph coexistence),
so a raw <button> in the panel fell back to the browser default: a grey
buttonface fill and a 2px outset border. That is what made the suggested-
prompt rows, the header history/new-chat buttons, and the composer attach
button look like heavy grey cards instead of the clean rows in the Figma
frames.
Re-establish Preflight's button normalization (background transparent,
border 0 solid) in one @layer base rule scoped to #agent-panel-root and the
teleported reka surfaces (dialogs, dropdowns, drawer now carry .agent-scope).
The selector is wrapped in :where() so it stays at zero specificity and every
button's own bg-* / border utilities still win, so filled/bordered buttons
(the blue send button, bordered chips) render exactly as authored.
Verified live: no grey buttonface or outset border remains on any panel
button, and the send button keeps its accent background.
Bring the panel closer to the Figma design (it had been built to the
written requirements, not the frames):
- The Comfy mark renders in brand yellow (text-brand-yellow), not the
generic blue accent, in both the header and the centered empty-state
mark.
- The panel title is "Comfy Agent" (Figma) rather than "AI Agent" (the
written doc, which disagreed with the design).
- Suggested prompts are light rows with a leading icon instead of heavy
bordered cards, per Figma B5 ("leading icon + label"). The per-prompt
icons are best-fit lucide icons pending the exact Figma icon set.
Typecheck, oxlint, and eslint clean; agent and core tests pass.
- Local dev visibility: the panel is gated on the PostHog flag
agent-in-app-experience, but that flag lives in a cloud PostHog project
the local dev build does not read, so the panel never appeared on
localhost. Force the gate open when the mode is development, so the
panel is visible on the dev server without wiring the flag; test and
production builds still gate on it (fail-closed unchanged).
- Empty-state overflow: the centered Comfy mark made the empty state
taller than the panel at the narrow default width, and justify-end
pushed the overflow up into the header (the greeting overlapped the
title). Use a scroll container with mt-auto so the mark + greeting
center when they fit and the whole thing scrolls when they do not,
instead of overlapping the header.
Flag-gate tests still pass with the mode guard; typecheck, oxlint, and
eslint clean.
Design B2 wants the panel to push the canvas and be drag-resizable (the
~30% / ~50% docks). The host sidebar already provides both - it is a
splitter panel (LiteGraphCanvasSplitterOverlay) that pushes the canvas
and drag-resizes, the same as the Assets panel. The agent panel was
fighting that by capping its own content width (max-w-md / max-w-2xl,
mx-auto): dragging the sidebar wider only added gutters instead of
widening the panel, and the Medium/Large toggle changed the cap rather
than the dock.
Remove the self-imposed width cap so the panel fills the resizable
sidebar, and drop the Medium/Large size toggle (the design struck the
width-toggle from B3; the host's native drag-resize replaces it). The
sizeMode prop drove nothing else - the empty-state layout never used it -
so this is a net removal: the toggle button, its emit chain across
AgentPanel and PanelHeader, the panelWidth computed, the now-unused cn
import, and the orphaned expand/collapse locale keys all go.
Typecheck, oxlint, eslint, and knip clean; 900 agent/core tests pass.
From a refresh of the V0 design spec (Notion "In-App Agent V0 - Design &
Requirements", Section A + B5) against the shipped panel:
- Branding: the design pins "the original Comfy mark" everywhere; the
panel used a lucide sparkles placeholder. Swap to the square Comfy
mark (icon-[comfy--comfy-c]) on all four surfaces - the sidebar tab,
the top-bar "Ask Comfy Agent" button, the panel header title row, and
the empty-state mark.
- Empty state: B5 requires a centered Comfy mark with a glow above the
greeting; it was missing. Add it (accent-tinted, currentColor glow).
- Greeting: B5/Section A want the account first name ("Hello Jo,"); the
panel rendered "Hello there," because the name was never wired. Pull
the first name from useCurrentUser and pass it through to the greeting,
falling back to the existing neutral default when there is no name.
Tests: assert the Comfy mark on both the tab registration and the top-bar
button; add a greeting-personalization test; loosen the e2e greeting
assertion to the stable "Hello" prefix so it survives personalization.
Typecheck, browser typecheck, oxlint, eslint, and knip clean; 1159 unit
tests pass.
The server owns the workflow: it creates one when a message opens a
thread and returns its id in the 202 ack (confirmed by the live-captured
wire fixtures, where every postMessage ack carries the same
workflow_id). The panel previously minted its own session workflow via
POST /api/workflows before the first send, which was redundant and
carried a latent bug: resuming a persisted thread would mint a fresh
workflow whose id never matches the thread's real one, so every
draft_patch would be foreign-dropped and the canvas would never update.
Now sendMessage posts without a workflow id and binds the draft store to
ack.workflow_id (bind is idempotent for an unchanged id, so re-binding
per ack never wipes an in-progress draft). The whole minting subsystem
goes away: agentWorkflowBinding and its tests, the ensure-before-send
in the root, the mint-failure toast and its i18n key, and the
/api/workflows route mock in the e2e mocks. start() no longer baselines
the draft; resync still fires on reconnect and behind-heartbeats, both
of which no-op until a send has bound a workflow, matching the previous
effective behavior.
zAgentTurnAccepted declares the optional workflow_id it always carried
through passthrough. Session and root tests rebind via the ack; the
root draft-binding test proves ack id -> draft_patch -> loadGraphData
and that a foreign-workflow patch is ignored.
Net -248 lines. Typecheck, browser typecheck, oxlint, eslint, and knip
clean; 1158 unit tests pass.
Findings from a four-hat panel, a QA-coverage gate, and three fail-fast
gates, each fixed with its test:
- Button.vue uses the existing cva (object API) instead of the branch's
class-variance-authority add, matching the rest of src; the root drops
that dependency (apps/website keeps its own pre-existing use). shiki
stays as the genuinely new dep for CodeBlock highlighting.
- copy-as-markdown now works: the active conversation registers as the
one current history row, so the drawer is not empty mid-chat and the
copy action has a target. Past sessions remain a documented V0 limit.
- onSend warns once via a toast when the session workflow cannot be
minted, so the draft loop no longer fails silently while the send
still proceeds unbound.
- the cloud e2e spec sources its suggested-prompt assertion from the
bundled locale, so it cannot drift from the rendered prompts again.
- the thread id moves into the conversation store so it survives a
panel remount (a sidebar toggle unmounts the panel), and the in-flight
turn is cancelled on unmount; together these stop a reopened panel
from splitting the transcript across two threads or leaving a turn
billing unheard.
- remove dead surface: createWebSocketEventSource, the workflow
binding's unused reset() and workflowName knob, an unused apply
version parameter, four unreferenced i18n keys, and a stale comment.
Typecheck, browser typecheck, oxlint, eslint, and knip are clean; the
unit suite is 1163 passing.
Three @cloud Playwright scenarios in browser_tests/tests/agent, using
the comfyPageFixture + webSocketFixture merge and typed mocks copied
from the live-captured wire fixtures:
- fail-closed flag gate: no sidebar tab or top-bar button without the
PostHog flag; both appear with it (flag seeded via
posthog_config.bootstrap.featureFlags with flags disabled-fetch, so
the gate resolves synchronously and deterministically)
- a full streamed turn: composer submit posts the message, the 202 ack
adopts the server message id, injected agent_thinking, tool-call,
delta, and done frames render thinking, tool cards, and markdown
(bold asserted as rendered strong)
- the draft loop: the first send mints the session workflow (mocked
POST /api/workflows), the bound draft_patch applies through
validateComfyWorkflow into the canvas, asserted by node count via
window.app.graph
The spec sends before pushing draft frames because the draft store
drops patches for an unbound workflow id, matching the binding order in
AgentPanelRoot. Draft content is a minimal schema-valid v0.4 graph
(the raw captured draft omits the top-level version field the validator
requires first).
Runs in the cloud project only (the extension is tree-shaken from
non-cloud builds); listed 3/3 there and 0 in chromium. Browser
typecheck, eslint, and format clean.
Acceptance items from the approved In-App Agent design doc, plus PM-98
and the coverage-audit gaps:
- suggested prompts carry the five exact approved labels
- an Ask Comfy Agent top-bar button registers next to the feedback
button, present only while the flag gate has the tab registered (same
fail-closed source, reactive mirror ref); click toggles the sidebar
tab
- attach is wired end-to-end: paperclip opens a picker (image/video),
files upload through rest.uploadImage via useAttachment (20MB guard,
failure does not stage), staged chips carry the server ref into the
message attachments; the STAGED marker drops from useAttachment
- drafts bind to a session workflow: agentWorkflowBinding mints a
server workflow (single-flight POST /api/workflows with the current
canvas graph, tolerant id parse, degrade-to-unbound on any failure)
and the root awaits ensure() before the first send so messages carry
workflow_id and draft_patch events resolve against a bound draft; the
FE has no host uuid surface today (path-keyed workflow classes,
/api/workflows unused in src), so the binding mirrors the backend's
own flow with a TODO to adopt the host id when one exists
- a muted composer caption states the agent-writes/user-runs contract
(copy pending design sign-off, one-line locale swap)
- a scroll-to-latest pill floats over the conversation when scrolled up
- history copy-as-markdown works for the active conversation via a pure
transcript builder; other sessions toast the V0 limitation
- message ratings capture app:agent_message_feedback through a typed
telemetry method (types, registry dispatch, PostHog implementation),
null vote forwarded as a retraction for the eval pipeline
- coverage gaps closed: DST-boundary recency bucketing, history store
upsert/remove/setActive behaviors, stopTurn network-error notice,
direct useComposer suite; the session error counter moves off module
scope into the instance; the dead null-tab rerender line becomes a
real assertion
Typecheck clean, full suite 12588 passed, oxlint, eslint, and knip
green on these files. 27 tests added.
Review fixes from the four-hat panel, each independently specified:
- the event source now follows the host socket across reconnects:
createReconnectingEventSource rebinds message and open/close listeners
to the fresh api.socket on the host reconnected event, reports live on
an already-open rebind, warns once on a null socket and attaches on
the first reconnect; AgentPanelRoot uses it in place of the
instance-bound adapter (the panel no longer goes deaf after a routine
/ws reconnect)
- the fail-closed flag gate in the extension entry gains a four-case
test suite modeled on the previewAny sibling (no register while the
flag is unknown, register once on true, unregister on flip-off, no
double-register across toggles)
- session notices forward to the host toast store (new-entries-only
watch), making cancel failures and malformed-event warnings visible;
covered by a mount test driving a malformed frame through a fake
socket
- the five staged M4 surfaces (tab context, attachments, canvas
selection) carry explicit STAGED markers and the subtree gains a
README recording them plus the intentional local ui-primitives
decision
- onboarding coach copy moves to i18n keys; the storage key and root id
drop their dev-harness names (Comfy.AgentPanel.onboarded,
agent-panel-root)
- the composer paperclip hides behind a default-false canAttach prop
until an attach flow is wired
The workflowId provider (panel-to-draft binding) is intentionally NOT
wired: no host surface exposes the server-side workflow uuid (the
workflow class is path-keyed; the uuid appears only in the agent's own
202 ack and per-job queue fields). Recorded as an open product/plumbing
decision.
Typecheck clean, full suite 12561 passed, oxlint and eslint clean, no
new knip findings from these files.
The panel now lives at src/workbench/extensions/agent (manager-extension
pattern) on host package versions, host pinia, and host vue-i18n; the
workspace package is gone. The sidebar tab becomes a plain type vue
registration (AgentPanelRoot rendered by the host app) and the
fail-closed PostHog flag gate in the extension entry is unchanged.
AgentPanelRoot is the host-context session root: workspace Cloud JWT as
the REST bearer, the host /ws socket wrapped as the event source (null
socket degrades to a warned no-op source), draft apply as a
schema-validated full-graph load, and the chrome the dev harness
carried (history drawer, starting-point modal, onboarding coach with
the same storage key and anchor) minus the dev fakes.
Theme tokens route through host semantics instead of hardcoded values:
the feature-owned agentTheme.css aliases every agent-* token to a host
token via @theme inline (pairing table documented inline), so the panel
tracks host theme flips including light mode, an intentional divergence
from the dark-only Figma frames deferred to the FE-1187 design pass.
Literals remain only for accent-fg, pill, and radius, each flagged as a
design-review item. The design-system master is untouched; the host
style entry gains one import line.
Locale keys merge under a top-level agent block in the host main.json.
cn comes from @comfyorg/tailwind-utils. shiki and
class-variance-authority join the catalog and root deps (CodeBlock
highlighting, Button variants). Dead-in-src modules are deleted rather
than knip-ignored: the shelved agentCore kernel and its tests, the
MinimizedBall dock (superseded by the sidebar form factor), and six
never-wired components knip flagged with zero consumers; unused schema
type exports trimmed likewise.
Gates: typecheck clean, moved tree 147/147 (197 minus the deleted
kernel's 50 cases; no surviving test lost), full unit suite 12550
passed, oxlint, eslint, oxfmt, and knip all green. Handoff invariants
re-verified in the moved tree: fixture schema gate, event transport
acceptance, draft monotonic adoption and canvas-apply seam, fail-closed
gate, entries interleave, send/stop/new-chat paths.
Known follow-ups (FE-1187): browser visual pass on the aliased tokens
and light mode, socket-reconnect rebinding, workflowId provider for
draft binding, design alignment.
The panel now runs the way everything user-facing runs here: as an
extension. src/extensions/core/agentPanel.ts (cloud-only, loaded from
the core extension index) registers a sidebar tab gated by the PostHog
flag agent-in-app-experience, fail-closed: the tab is not registered
until the flag evaluates true and unregisters if it turns off. The tab
is type custom: it mounts the panel as its own Vue app via the new
mountAgentPanel entry, which keeps the package's vue-i18n major (11)
and pinia instance isolated from the host (vue-i18n 9). Host wiring in
the entry: workspace Cloud JWT as the REST bearer (store instance
captured in host context, since pinia's active-instance global is
shared with the panel app), the host /ws socket wrapped by
createWebSocketEventSource, and draft apply as a schema-validated
full-graph load (validateComfyWorkflow then app.loadGraphData) per the
V0 tech design.
To make the package importable from src, it is properly library-ized:
all internal @/ alias imports rewritten to relative paths (the alias
would have resolved against the HOST src when host tooling compiles the
package sources), the alias config dropped, and a public entry exposed
via package.json exports (mount + the host-facing seam builders and
types). Root package.json declares the workspace dependency like every
other @comfyorg package.
Known follow-ups tracked in FE-1187: socket-reconnect rebinding for the
event source, confirming agent_* frames reach raw socket listeners,
the workflowId provider for draft binding, panel styling inside the tab
(tailwind theme integration), and the design-alignment pass.
Package gates green (typecheck, 197 tests, build); host root typecheck
green with the entry included.
Bulk copy of the panel from Comfy-Org/comfy-inapp-agent (branch
feat/agent-panel-services at b3675fc) into packages/agent-panel
(@comfyorg/agent-panel), per the decision to home the panel in this
repo. The package is self-contained with its own dependency set and
configs; tooling versions align with the workspace catalog (vite 8,
vitest 4, zod ^3.23.8, typescript 5.9, vue-tsc 3.2), so the code
relocates verbatim. Excluded from the copy: the standalone project's
lockfile and nested workspace marker; the root allowBuilds already
carries vue-demi: false. The unused @ai-sdk/vue dependency was dropped.
Contents: the comfy-agent v1 client stack (zod wire contract with
live-captured fixtures, REST client for ingest /api/agent/*, WebSocket
event transport and event-source adapter, server-draft store plus the
useDraftCanvasApply seam, useAgentSession composition root), the
fail-closed PostHog gate for the agent-in-app-experience flag, the full
chat panel UI (message stream, composer, chrome, shelved safety
surfaces), i18n, and a dev harness with an offline scripted mode and a
live proxy mode verified against pr-4432.testenvs.comfy.org.
Package gates green in the workspace: typecheck clean, 197 tests
passing, build succeeds.
Next (B8): mount the panel in the app, wire the host socket, JWT,
PostHog client, and canvas draft-apply, then cut over from the legacy
extension.
2026-07-06 19:38:39 -07:00
450 changed files with 27366 additions and 6705 deletions
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
post_comment "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist" "missing branch ${target}"
elif [ "${reason}" = "already-exists" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
post_comment "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed." "already-backported ${target}"
elif [ "${reason}" = "branch-create-failed" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` failed: could not create the backport branch. Please retry or backport manually."
elif [ "${reason}" = "push-failed" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` cherry-picked cleanly but the push failed. Please retry or push the backport branch manually."
en:'Custom-node packs on Comfy Cloud — supported by default',
en:'Custom-node packs on Comfy Cloud - supported by default',
'zh-CN':'Comfy Cloud 自定义节点包合集——开箱即用'
},
'cloudNodes.meta.description':{
@@ -1845,8 +1845,8 @@ const translations = {
// MCP – Meta
'mcp.meta.title':{
en:'Comfy MCP — Drive ComfyUI from any AI agent',
'zh-CN':'Comfy MCP — 让任何 AI 智能体驱动 ComfyUI'
en:'Comfy MCP - Drive ComfyUI from any AI agent',
'zh-CN':'Comfy MCP - 让任何 AI 智能体驱动 ComfyUI'
},
'mcp.meta.description':{
en:'Comfy MCP exposes the full ComfyUI engine over the Model Context Protocol. Generate images, video, audio, and 3D from Claude Code, Claude Desktop, and any MCP-compatible client.',
@@ -3483,8 +3483,8 @@ const translations = {
},
'affiliate-terms.page.title':{
en:'Affiliate Terms — Comfy',
'zh-CN':'Affiliate Terms — Comfy'
en:'Affiliate Terms - Comfy',
'zh-CN':'Affiliate Terms - Comfy'
},
'affiliate-terms.page.description':{
en:'Comfy.org Affiliate Program Terms and Conditions.',
@@ -3896,8 +3896,8 @@ const translations = {
'This document reproduces the current template of the Enterprise Customer Agreement for reference only. The executed Agreement between Comfy and Customer, together with any signed Order Forms, governs the relationship between the parties. To request an executable copy, please contact <a href="mailto:sales@comfy.org" class="text-white underline">sales@comfy.org</a>.'
},
'enterprise-msa.page.title':{
en:'Enterprise MSA — Comfy',
'zh-CN':'Enterprise MSA — Comfy'
en:'Enterprise MSA - Comfy',
'zh-CN':'Enterprise MSA - Comfy'
},
'enterprise-msa.page.description':{
en:'Comfy Enterprise Customer Agreement — the master services agreement that governs Comfy Enterprise deployments of Comfy Cloud, Comfy API, and related products.',
@@ -4361,8 +4361,8 @@ const translations = {
// Affiliate page (/affiliates) — head metadata
'affiliate.page.title':{
en:'Comfy.org Affiliate Program — Become a Partner',
'zh-CN':'Comfy.org 联盟计划 — 成为合作伙伴'
en:'Comfy.org Affiliate Program - Become a Partner',
'zh-CN':'Comfy.org 联盟计划 - 成为合作伙伴'
},
'affiliate.page.description':{
en:'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.',
@@ -4387,8 +4387,8 @@ const translations = {
// Launches page (/launches) — head metadata
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'launches.page.title':{
en:'ComfyUI Live Demo & Q&A — June 29 Launch Livestream',
'zh-CN':'ComfyUI 直播演示与问答 — 6 月 29 日发布直播'
en:'ComfyUI Live Demo & Q&A - June 29 Launch Livestream',
'zh-CN':'ComfyUI 直播演示与问答 - 6 月 29 日发布直播'
},
'launches.page.description':{
en:'Join the ComfyUI livestream on June 29 for a hands-on product demo and live Q&A. See what’s new across desktop, cloud, and community, and get your questions answered.',
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.