Skip to content

Commit 6f79cf4

Browse files
committed
feat(agent): experimental in-browser ComfyAI agent
Squashes 60 incremental agent commits into one cohesive change after prior history accumulated 119 unrelated upstream commits during a bad rebase. Working tree restored from sno-agent@aec9f9380; new deps added freshly (no stale lockfile entries); changeTracker API call sites updated for current sno-frontend-preview (captureCanvasState → checkState). What's in this commit: UI: src/agent/ui/{AgentRoot,AgentFab,XtermPanel,AgentSettings, useXtermReadline}.vue - Floating ComfyAI button + draggable xterm panel toggled via 'c' keybind; auto-focuses the terminal helper textarea on open. - xterm-driven readline with Tab completion, Shift+Enter newline, Ctrl-A/E/U/K/L/C, history navigation, multiline buffers. - Liquid-glass theme using Comfy design tokens. - Settings panel auto-opens when no API key set; compact 3-field layout (API base / API key / model) + collapsed advanced section. Session loop: src/agent/llm/session.ts, composables/useAgentSession.ts - Vercel AI SDK streamText with run_shell as the only tool. - IndexedDB-persisted message history (300-message cap), replays on reopen with a 'previous session' divider. - Programmatic guardrails: PROMISSORY_PATTERN auto-continue, silent-fail auto-continue, fragile-shell-idiom blocklist (Layer 1), definitive-claim verifier registry (Layer 2 — orphans, missing-models, queue-state, pre-refusal, punt-to-user). - Configurable baseURL for OpenRouter / local LLM proxies; default gpt-5.4 via OpenAI. Shell runtime: src/agent/shell/ - POSIX-ish parser (shell-quote based), AsyncIterable pipes, redirection, &&/||/; sequencing. - VFS: in-memory /tmp + UserdataVFS-backed /workflows. - Coreutils + Comfy.* command dispatch + run-js fallback. Commands: src/agent/shell/commands/ - comfy/comfyNs: registered command discovery + namespace dispatch. - workflow: save-as / new-workflow / rename-workflow / clear-workflow --force / set-subgraph-{desc,aliases} (modal-bypass equivalents). - nodeOps: node-search, add-node (smart placement), connect (auto- layout on link), disconnect, remove-node, layout, align-nodes, distribute-nodes, select, get-widget, toggle-panel. - graph/state/execution/sweep: introspection + queue + sweep helpers. - templates / images / install / validate / see: template loading, output→input copy, Manager model install, Gemini canvas vision. Tests: browser_tests/tests/agentTerminal.spec.ts, plus src/agent/**/*.test.ts unit tests. Docs: docs/adr/0009-frontend-only-agent-and-local-agent-bridge.md records the architectural choice + rejected alternatives. i18n: minimal en additions in src/locales/en/main.json for agent.{fab,input,panel,settings} namespaces. Deps added: @ai-sdk/openai, ai, zod, shell-quote, idb-keyval, @xterm/xterm, @xterm/addon-fit, es-toolkit, @types/shell-quote. PR #11547 — experimental, draft, expect breaking changes. Preview: https://pr-11547.comfy-ui.pages.dev/
1 parent d840020 commit 6f79cf4

54 files changed

Lines changed: 9450 additions & 58 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { expect } from '@playwright/test'
2+
3+
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
4+
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
5+
6+
/**
7+
* E2E coverage for the in-browser agent terminal (AgentFab + XtermPanel).
8+
*
9+
* We exercise the deterministic surface: FAB → open/close, the solid-block
10+
* ASCII banner, the "COMFY-AI" title pill, keyboard affordances
11+
* (Enter submits, Shift+Enter inserts a literal newline, Tab completes),
12+
* and a representative slice of shell commands that back the examples
13+
* documented in the LLM system prompt. We intentionally do NOT drive the
14+
* LLM itself — typing into xterm runs the shell directly, which is what
15+
* the model ends up calling via the `run_shell` tool.
16+
*/
17+
18+
async function openPanel(comfyPage: ComfyPage): Promise<void> {
19+
const fab = comfyPage.page.getByTestId('agent-fab')
20+
await expect(fab).toBeVisible()
21+
await fab.click()
22+
await expect(comfyPage.page.getByTestId('agent-panel')).toBeVisible()
23+
}
24+
25+
async function readTerminalText(comfyPage: ComfyPage): Promise<string> {
26+
// xterm renders into rows under .xterm-rows; concatenate the text content.
27+
return await comfyPage.page
28+
.getByTestId('agent-terminal')
29+
.locator('.xterm-rows')
30+
.innerText()
31+
}
32+
33+
async function typeAndEnter(comfyPage: ComfyPage, text: string): Promise<void> {
34+
const helper = comfyPage.page
35+
.getByTestId('agent-terminal')
36+
.locator('.xterm-helper-textarea')
37+
await helper.focus()
38+
await comfyPage.page.keyboard.type(text)
39+
await comfyPage.page.keyboard.press('Enter')
40+
}
41+
42+
test.describe('Agent terminal', { tag: '@ui' }, () => {
43+
test('FAB opens the panel and shows the COMFY-AI title + prompt', async ({
44+
comfyPage
45+
}) => {
46+
await openPanel(comfyPage)
47+
48+
await expect(comfyPage.page.getByTestId('agent-panel-title')).toHaveText(
49+
'COMFY-AI'
50+
)
51+
52+
// Banner is suppressed for token efficiency — only the shell prompt
53+
// should be rendered once the terminal is ready.
54+
await expect.poll(() => readTerminalText(comfyPage)).toMatch(/comfy>/)
55+
})
56+
57+
test('Clicking the FAB again closes the panel', async ({ comfyPage }) => {
58+
await openPanel(comfyPage)
59+
await comfyPage.page.getByTestId('agent-fab').click()
60+
await expect(comfyPage.page.getByTestId('agent-panel')).toBeHidden()
61+
})
62+
63+
test('Enter submits; help command lists built-ins', async ({ comfyPage }) => {
64+
await openPanel(comfyPage)
65+
await typeAndEnter(comfyPage, 'help')
66+
await expect
67+
.poll(() => readTerminalText(comfyPage))
68+
.toMatch(/run-js|cmd-list|comfy/)
69+
})
70+
71+
test('Shift+Enter inserts a literal newline (no submit)', async ({
72+
comfyPage
73+
}) => {
74+
await openPanel(comfyPage)
75+
const helper = comfyPage.page
76+
.getByTestId('agent-terminal')
77+
.locator('.xterm-helper-textarea')
78+
await helper.focus()
79+
await comfyPage.page.keyboard.type('echo one')
80+
await comfyPage.page.keyboard.press('Shift+Enter')
81+
await comfyPage.page.keyboard.type('echo two')
82+
// Still not submitted → now submit the whole multiline buffer.
83+
await comfyPage.page.keyboard.press('Enter')
84+
85+
const out = await readTerminalText(comfyPage)
86+
// Both commands ran sequentially via the runtime.
87+
expect(out).toContain('one')
88+
expect(out).toContain('two')
89+
})
90+
91+
test('Tab completes a partial command', async ({ comfyPage }) => {
92+
await openPanel(comfyPage)
93+
const helper = comfyPage.page
94+
.getByTestId('agent-terminal')
95+
.locator('.xterm-helper-textarea')
96+
await helper.focus()
97+
// "des" → unique prefix of `describe`
98+
await comfyPage.page.keyboard.type('des')
99+
await comfyPage.page.keyboard.press('Tab')
100+
// Submit; describe w/o arg yields usage text on stderr.
101+
await comfyPage.page.keyboard.press('Enter')
102+
await expect
103+
.poll(() => readTerminalText(comfyPage))
104+
.toMatch(/usage: describe/)
105+
})
106+
107+
test('coreutils: pwd / echo / true / false', async ({ comfyPage }) => {
108+
await openPanel(comfyPage)
109+
await typeAndEnter(comfyPage, 'pwd')
110+
await expect.poll(() => readTerminalText(comfyPage)).toMatch(/^\//m)
111+
112+
await typeAndEnter(comfyPage, 'echo hello world')
113+
await expect
114+
.poll(() => readTerminalText(comfyPage))
115+
.toContain('hello world')
116+
})
117+
118+
test('comfy namespace lists subcommands', async ({ comfyPage }) => {
119+
await openPanel(comfyPage)
120+
await typeAndEnter(comfyPage, 'comfy')
121+
await expect
122+
.poll(() => readTerminalText(comfyPage))
123+
.toMatch(/ComfyUI command namespace/)
124+
})
125+
126+
test('run-js evaluates in the page scope', async ({ comfyPage }) => {
127+
await openPanel(comfyPage)
128+
await typeAndEnter(comfyPage, 'run-js return 1 + 2')
129+
await expect.poll(() => readTerminalText(comfyPage)).toMatch(/\b3\b/)
130+
})
131+
132+
test('graph summary reports node count for the active graph', async ({
133+
comfyPage
134+
}) => {
135+
await openPanel(comfyPage)
136+
await typeAndEnter(comfyPage, 'graph summary')
137+
await expect
138+
.poll(() => readTerminalText(comfyPage))
139+
.toMatch(/node|count|nodes/i)
140+
})
141+
142+
test('queue-status command returns output', async ({ comfyPage }) => {
143+
await openPanel(comfyPage)
144+
await typeAndEnter(comfyPage, 'queue-status')
145+
await expect
146+
.poll(() => readTerminalText(comfyPage))
147+
.toMatch(/running|pending|queue/i)
148+
})
149+
150+
test('active-workflow reports path / state', async ({ comfyPage }) => {
151+
await openPanel(comfyPage)
152+
await typeAndEnter(comfyPage, 'active-workflow')
153+
await expect
154+
.poll(() => readTerminalText(comfyPage))
155+
.toMatch(/path|modified|persisted|none/i)
156+
})
157+
158+
test('pipe: echo foo | wc -c emits a byte count', async ({ comfyPage }) => {
159+
await openPanel(comfyPage)
160+
await typeAndEnter(comfyPage, 'echo foo | wc -c')
161+
// "foo\n" = 4 bytes
162+
await expect.poll(() => readTerminalText(comfyPage)).toMatch(/\b4\b/)
163+
})
164+
165+
test('unknown command surfaces an error', async ({ comfyPage }) => {
166+
await openPanel(comfyPage)
167+
await typeAndEnter(comfyPage, 'definitely-not-a-real-command-xyz')
168+
await expect
169+
.poll(() => readTerminalText(comfyPage))
170+
.toMatch(/not found|unknown|no such/i)
171+
})
172+
})
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# 9. Frontend-only In-app Agent + Future Local-Agent Bridge
2+
3+
Date: 2026-04-26
4+
5+
## Status
6+
7+
Proposed
8+
9+
## Context
10+
11+
PR #11547 introduces an experimental in-browser agent (`ComfyAI`) that
12+
lets users drive ComfyUI with natural language. It lives entirely in
13+
`src/agent/` and runs in the SPA — prompt assembly, tool execution
14+
(browser-side `run-js` + Comfy API calls), message storage, and IndexedDB
15+
chat history all happen client-side. The LLM is reached directly from
16+
the browser via the user's API key (OpenAI / OpenRouter / any
17+
OpenAI-compatible gateway), with optional Comfy Cloud auth for the
18+
small set of cloud nodes (Tripo / Tencent / Meshy / Gemini).
19+
20+
This frontend-only architecture is deliberate. It keeps the deployment
21+
story trivial (no backend changes), keeps the user's API key out of
22+
ComfyUI's backend, and works whether the backend is local or remote.
23+
But it raises a coordination problem the moment users want their
24+
**other agents** — Claude Code, a self-hosted CLI agent, a teammate's
25+
agent on a different machine — to participate in the same conversation,
26+
see the same workflow state, or take actions on the user's behalf.
27+
28+
The forces at play:
29+
30+
- **Privacy**: API keys must not leak to ComfyUI's backend or to other
31+
observers. The frontend-only model makes this trivially true today.
32+
- **Source of truth for graph state**: the canonical workflow lives in
33+
LiteGraph's in-memory tree inside the SPA. Backend has the queue +
34+
history but doesn't track unsaved edits. Any other agent that wants
35+
current state must either read from the SPA or read a snapshot the
36+
SPA publishes.
37+
- **Tool affordance**: the agent's `run_shell` tool currently executes
38+
in the browser page context (DOM, stores, fetch with same-origin
39+
cookies). A local agent has none of that — it would need either a
40+
separate REST surface or to drive the SPA remotely.
41+
- **Identity**: the SPA can hold a Comfy Cloud token; a local agent is
42+
a separate principal and should hold its own credentials.
43+
- **Versioning**: the moment we expose a wire format, breaking changes
44+
hurt. Whatever we ship first becomes the contract.
45+
46+
The question this ADR exists to answer: **how should a local agent
47+
participate in the in-app agent's session, given the frontend-only
48+
constraint we want to preserve?**
49+
50+
## Decision
51+
52+
**Short term (this PR and the next few): keep the agent strictly
53+
frontend-only.** Do not add any backend session state, message
54+
relaying, or local-agent bridge. The current architecture is small,
55+
auditable, and removes whole categories of risk.
56+
57+
**Long term: when local-agent integration is taken on, prefer Option C
58+
("opt-in publish bus with execution staying in the SPA") over the
59+
alternatives.** The detailed shape:
60+
61+
1. Define a small JSON-RPC schema for "agent context" — current
62+
workflow id + serialized graph, last N messages, last K tool
63+
invocations, agent settings (model + base URL only, never key).
64+
Versioned from the start.
65+
2. SPA exposes a "Share session" toggle in agent settings. When on,
66+
it publishes that snapshot to a configurable WS endpoint
67+
(default: `ws://localhost:7437/agent`). The user explicitly opts
68+
in per session.
69+
3. Provide a tiny reference subscriber library that local agents use
70+
to consume. They get **read-only access by default**; getting
71+
write access (post a message back into the user's panel) requires
72+
the SPA to authorize via a one-time pairing code shown to the
73+
user.
74+
4. **Tool execution stays in the SPA.** Local agents can _propose_
75+
actions ("run this run-js"); the SPA executes and streams the
76+
result back. The local agent is a peer that suggests, not an
77+
actor that mutates.
78+
79+
**Alternatives considered and rejected (for now):**
80+
81+
- **Option A — ComfyUI backend as session broker.** Push messages to
82+
the running ComfyUI server, local agents subscribe via WS or
83+
polling. Rejected because ComfyUI is meant to be largely stateless,
84+
adding session storage is scope creep, and it puts API keys / chat
85+
content in front of the backend (privacy regression).
86+
- **Option B — browser extension or local sidecar daemon.** A
87+
companion daemon reads the SPA's IndexedDB via Chrome DevTools
88+
Protocol, or the SPA opens a localhost WS to it. Rejected as the
89+
default path because of the cross-platform packaging burden and
90+
because it doesn't help when the local agent runs on a different
91+
machine than the SPA.
92+
93+
**Comfy Cloud creds reuse (a related future work item):** when the
94+
user is signed into Comfy Cloud (the `auth_token_comfy_org` flow we
95+
already use for Tripo/Gemini), the agent could optionally route LLM
96+
calls through a Comfy-managed inference endpoint instead of OpenAI
97+
direct. This would gate naturally on the same auth as the cloud
98+
nodes and simplifies onboarding for users who don't have their own
99+
OpenAI/OpenRouter key. Out of scope here, but worth noting because
100+
it interacts with the local-agent identity story above.
101+
102+
## Consequences
103+
104+
### Positive
105+
106+
- **No backend changes today.** PR #11547 lands without touching
107+
ComfyUI core. Reviewers don't need to evaluate session-state
108+
infrastructure they didn't ask for.
109+
- **Privacy posture stays strong.** API keys + chat content stay in
110+
the user's browser; ComfyUI backend continues to see only what it
111+
always saw (queue prompts, file uploads).
112+
- **Future local-agent path is clear** without committing to a
113+
protocol prematurely. When we build it, the SPA stays the
114+
source-of-truth + execution sandbox; the local agent is a peer that
115+
suggests. Mirrors how editors coexist with Claude Code, GitHub
116+
Copilot, etc.
117+
- **Headroom for multi-subscriber.** Option C naturally supports
118+
agent + observer + log-tap subscribers with the same protocol —
119+
useful for future debugging tools.
120+
- **Versioned wire format** means breaking changes are explicit.
121+
122+
### Negative
123+
124+
- **Local agents have no participation today.** Users who want their
125+
Claude Code session to see what they're doing in ComfyUI need to
126+
copy/paste workflow JSON manually.
127+
- **When we do build the bridge, it's net-new infrastructure** — a
128+
WS server, a pairing flow, a versioning policy, a reference
129+
subscriber library. Not trivial.
130+
- **Tool execution stays in the SPA** even after the bridge ships,
131+
which means a local agent on a different machine can't `run-js`
132+
against the user's session without the SPA being open. (We accept
133+
this as a privacy + simplicity tradeoff.)
134+
- **The "Share session" toggle is yet another decision the user has
135+
to make**, with non-obvious risks. Mitigations: clear UX copy,
136+
default off, pairing-code requirement for write access.
137+
138+
## Notes
139+
140+
- The frontend-only constraint also drove several smaller decisions
141+
in the PR that are worth recording briefly:
142+
- Reasoning guardrails (`PROMISSORY_PATTERN`, `vetScript`,
143+
`verifyClaims`) live in the SPA in `src/agent/llm/session.ts`,
144+
not in a separate service. They survive prompt drift because
145+
they're code, not text.
146+
- Chat history is persisted via `useIDBKeyval` to IndexedDB. This
147+
is a per-browser-profile store; switching profiles or clearing
148+
site data wipes history. Acceptable for the experimental phase;
149+
if local-agent bridge ships, the snapshot the SPA publishes
150+
becomes another effective "external" history mechanism.
151+
- The default LLM is `gpt-5.4` via OpenAI's official API. The
152+
settings panel exposes a base-URL field so users can target
153+
OpenRouter (`https://openrouter.ai/api/v1`) or any OpenAI-compatible
154+
gateway. This base-URL flexibility also makes Option C's "Comfy
155+
Cloud as inference endpoint" trivially achievable later — it's just
156+
another base-URL choice.
157+
- Concrete near-term TODOs flagged by this PR's stress-testing,
158+
_not_ covered by this ADR but related:
159+
- Layer 3 of the reasoning guardrails (structured JSON answers
160+
with provenance) needs SDK plumbing to surface tool-call IDs
161+
alongside text. Currently deferred.
162+
- Verifier registry and shell-idiom blocklist are open
163+
registries; entries grow as new failure modes surface in real
164+
use.

docs/adr/README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ An Architecture Decision Record captures an important architectural decision mad
88

99
## ADR Index
1010

11-
| ADR | Title | Status | Date |
12-
| -------------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
13-
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
14-
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
15-
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
16-
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
17-
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
18-
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
19-
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
20-
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
11+
| ADR | Title | Status | Date |
12+
| ---------------------------------------------------------- | ------------------------------------------------------ | -------- | ---------- |
13+
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
14+
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
15+
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
16+
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
17+
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
18+
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
19+
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
20+
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
21+
| [0009](0009-frontend-only-agent-and-local-agent-bridge.md) | Frontend-only In-app Agent + Future Local-Agent Bridge | Proposed | 2026-04-26 |
2122

2223
## Creating a New ADR
2324

0 commit comments

Comments
 (0)