mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-13 17:26:22 +00:00
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/
93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
import { api } from '@/scripts/api'
|
|
|
|
import type { IngestedAsset } from '../stores/agentStore'
|
|
|
|
interface IngestResult {
|
|
asset: IngestedAsset
|
|
remote: boolean
|
|
}
|
|
|
|
function safeName(raw: string): string {
|
|
return raw.replace(/[^\w.-]+/g, '_').slice(0, 120) || `pasted_${Date.now()}`
|
|
}
|
|
|
|
function detectExt(mime: string): string {
|
|
if (mime === 'image/png') return '.png'
|
|
if (mime === 'image/jpeg') return '.jpg'
|
|
if (mime === 'image/webp') return '.webp'
|
|
if (mime === 'image/gif') return '.gif'
|
|
if (mime === 'text/plain') return '.txt'
|
|
return ''
|
|
}
|
|
|
|
async function uploadToInput(file: File): Promise<string | null> {
|
|
const body = new FormData()
|
|
body.append('image', file, file.name)
|
|
body.append('type', 'input')
|
|
body.append('overwrite', 'false')
|
|
try {
|
|
const resp = await api.fetchApi('/upload/image', { method: 'POST', body })
|
|
if (!resp.ok) return null
|
|
const json = (await resp.json()) as { name?: string; subfolder?: string }
|
|
if (!json.name) return null
|
|
const prefix = json.subfolder ? `${json.subfolder}/` : ''
|
|
return `/input/${prefix}${json.name}`
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
interface AssetIngestOptions {
|
|
uploader?: (file: File) => Promise<string | null>
|
|
}
|
|
|
|
export function useAssetIngest(options: AssetIngestOptions = {}) {
|
|
const uploader = options.uploader ?? uploadToInput
|
|
|
|
async function ingestFile(file: File): Promise<IngestResult> {
|
|
const remotePath = await uploader(file)
|
|
const fallbackName =
|
|
file.name && file.name.length > 0
|
|
? safeName(file.name)
|
|
: safeName('pasted') + detectExt(file.type)
|
|
const path = remotePath ?? `/tmp/pasted/${fallbackName}`
|
|
const previewUrl = file.type.startsWith('image/')
|
|
? URL.createObjectURL(file)
|
|
: undefined
|
|
return {
|
|
asset: {
|
|
id: crypto.randomUUID(),
|
|
name: fallbackName,
|
|
path,
|
|
mime: file.type || 'application/octet-stream',
|
|
size: file.size,
|
|
previewUrl
|
|
},
|
|
remote: remotePath !== null
|
|
}
|
|
}
|
|
|
|
async function ingestFromClipboard(
|
|
data: DataTransfer | null
|
|
): Promise<IngestResult[]> {
|
|
if (!data) return []
|
|
const results: IngestResult[] = []
|
|
for (const item of Array.from(data.items)) {
|
|
if (item.kind !== 'file') continue
|
|
const file = item.getAsFile()
|
|
if (file) results.push(await ingestFile(file))
|
|
}
|
|
if (results.length === 0 && data.files && data.files.length > 0) {
|
|
for (const file of Array.from(data.files)) {
|
|
results.push(await ingestFile(file))
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
return {
|
|
ingestFile,
|
|
ingestFromClipboard
|
|
}
|
|
}
|