Skip to content
Open
Show file tree
Hide file tree
Changes from 76 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
15da279
Add dev mode, cpu-only linux support
leslielazzarino Jun 1, 2026
a11424c
Pre-install dependencies on linux
leslielazzarino Jun 2, 2026
20c37b9
Fix generated image display on Linux
leslielazzarino Jun 2, 2026
5097041
Detect Intel GPUs and fix OpenVino
leslielazzarino Jun 2, 2026
81ede25
wip: slack poc
mschuettlerTNG May 28, 2026
f36a90f
refactor: abstract home agent channels
mschuettlerTNG May 28, 2026
9d98324
fix: config loading
mschuettlerTNG May 28, 2026
ed7c860
feat: 3d, video, HA ux
mschuettlerTNG May 29, 2026
b0bebeb
fix: agentic video gen
mschuettlerTNG May 29, 2026
6f84d37
feat: add home agent STT
mschuettlerTNG May 29, 2026
d2e7cf2
feat: audio output
mschuettlerTNG May 30, 2026
0fd50e5
fix: strip md from tts
mschuettlerTNG Jun 1, 2026
4f611eb
wip: arch refactor
mschuettlerTNG Jun 1, 2026
962faaf
wip: activity display
mschuettlerTNG Jun 1, 2026
9528cae
fix: improve slack audio format
mschuettlerTNG Jun 2, 2026
8d577c8
fix: history button
mschuettlerTNG Jun 2, 2026
d2c2381
feat: home agent rag + settings
mschuettlerTNG Jun 3, 2026
9974af9
chore: improve ha tool calling
mschuettlerTNG Jun 3, 2026
e6b5282
Add Linux executable creation
leslielazzarino Jun 3, 2026
f8cadfa
feat: screenshot and web browser tools
mschuettlerTNG Jun 3, 2026
ac2d20c
feat: improve browsing feedback
mschuettlerTNG Jun 3, 2026
3c5e961
feat: forward errors to ha channel
mschuettlerTNG Jun 3, 2026
206fd4d
Clean up non-working features in the Linux executable creation code
leslielazzarino Jun 5, 2026
7f94b6f
Fix Ubuntu install of OpenVINO and ComfyUI
leslielazzarino Jun 8, 2026
6c9724a
chore: improve error handling
mschuettlerTNG Jun 9, 2026
85f8e2d
Simplify code for Ubuntu install of OpenVINO and ComfyUI
leslielazzarino Jun 9, 2026
814557a
Bake --no-sandbox in Ubuntu .AppImage
leslielazzarino Jun 9, 2026
2a22172
feat: single UAC prompt for Phison setup + large MoE preset
mschuettlerTNG Jun 9, 2026
26580bd
fix: correct sharded GGUF size/download and download dialog model type
mschuettlerTNG Jun 9, 2026
455c94a
chore: enable RAG and tools-by-default in Phison preset
mschuettlerTNG Jun 9, 2026
39d7342
chore: improve home agent UX
mschuettlerTNG Jun 9, 2026
da5d1d5
Add proxy support for Linux
leslielazzarino Jun 10, 2026
27228fd
Fix ComfyUI Linux startup issues
leslielazzarino Jun 10, 2026
038dfc2
Build .deb Ubuntu installer
leslielazzarino Jun 10, 2026
13346a9
chore: home agent fixes, backend device persistence
mschuettlerTNG Jun 10, 2026
08ad1af
fix: homeagent build config
mschuettlerTNG Jun 10, 2026
294f65b
fix: ovms version, tts model
mschuettlerTNG Jun 10, 2026
0aae307
fix: ovms path, revert 3d test preset
mschuettlerTNG Jun 10, 2026
7ad8ffa
Speed up home agent
leslielazzarino Jun 11, 2026
7229ac2
Fix telegram connection drops when opening Comfy
leslielazzarino Jun 12, 2026
28aba38
Improve logging
leslielazzarino Jun 12, 2026
f256ba1
Add 100MB cap to log size
leslielazzarino Jun 15, 2026
3bc45ac
Hide plus button when home agent history is shown.
leslielazzarino Jun 15, 2026
be633ac
Sync home agent toggle and preset
leslielazzarino Jun 15, 2026
3bbd5b6
Inform user about loading errors due to too large context size
leslielazzarino Jun 15, 2026
42c109c
Send error message to Telegram when model loading fails.
leslielazzarino Jun 15, 2026
492d30b
Fix auto-scroll
leslielazzarino Jun 16, 2026
b63ff5c
Merge slack-support-fixed into build-2025-06-17
leslielazzarino Jun 17, 2026
943007a
Keep the 2 most recent images in home agent prompt history
leslielazzarino Jun 17, 2026
191d6e3
Llama auto-recovery
leslielazzarino Jun 16, 2026
9ae76dd
Enable home agent in production
leslielazzarino Jun 17, 2026
bcb0454
Merge branch 'cursor/8c0a89f5' into build-2025-06-17
mschuettlerTNG Jun 17, 2026
103b6d8
chore: final test build adjustments
mschuettlerTNG Jun 17, 2026
9d0a9ab
Merge branch 'linux-poc' into 3.1.2-rc1-with-linux-poc
leslielazzarino Jun 22, 2026
f4faa80
Update dependencies 2026.06.22
leslielazzarino Jun 22, 2026
17eeac6
Update xpusmi to 2.0.0
leslielazzarino Jun 22, 2026
e0f80d4
Update qwen models
leslielazzarino Jun 22, 2026
d94268b
Improve tool parsing in OpenVINO
leslielazzarino Jun 22, 2026
58d46ba
linux poc identified gap fixes (#243)
guptagunjan Jun 22, 2026
014ed1a
Merge remote-tracking branch 'origin/linux-poc' into 3.1.2-rc1-improv…
mschuettlerTNG Jun 22, 2026
e3c722d
chore: add models
mschuettlerTNG Jun 22, 2026
d2c14e2
fix: correctly gate phison preset, split LTX t2v and i2v, test OV dyn…
mschuettlerTNG Jun 22, 2026
72e7da7
fix: ovms context display
mschuettlerTNG Jun 22, 2026
2b239bd
chore: update phison version
mschuettlerTNG Jun 22, 2026
2aac627
feat: add thinking toggle
mschuettlerTNG Jun 22, 2026
3a2afc8
fix: allow users to download comfyui models via home agent
mschuettlerTNG Jun 22, 2026
6f8777c
chore: fix phison download url
mschuettlerTNG Jun 22, 2026
5055516
chore: fix lint & format
mschuettlerTNG Jun 22, 2026
46b06f2
Persist version of downloaded resources
leslielazzarino Jun 23, 2026
4b10cfd
fix: backend settings dialog
mschuettlerTNG Jun 23, 2026
242c9f6
feat: add shortcut to home agent setup
mschuettlerTNG Jun 23, 2026
00264af
Update Comfy and Llama to latest
leslielazzarino Jun 23, 2026
f079a97
Merge remote-tracking branch 'origin/3.1.2-beta' into 3.1.2-rc2
leslielazzarino Jun 23, 2026
c079d64
Fix code issues from automatic scan
leslielazzarino Jun 23, 2026
1fe5c49
Fix 3 vulnerabilities
leslielazzarino Jun 24, 2026
2d7dafa
Remove unused .venv from installer
leslielazzarino Jun 24, 2026
ff1478f
Move dev deps listed as prod deps
leslielazzarino Jun 24, 2026
faef218
Apply CodeRabbit suggestions
leslielazzarino Jun 24, 2026
bf0a506
chore: switch ov tts to kokoro
mschuettlerTNG Jun 24, 2026
041e762
Fix Comfy after update
leslielazzarino Jun 24, 2026
04de908
Fix From Prompt 2 reference images bug
leslielazzarino Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ WebUI/external/workflows_bak/

*.7z
*.whl
*.tar.gz
hijacks/
ComfyUI/
ollama-service/
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.12.13
3 changes: 2 additions & 1 deletion WebUI/build/build-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
},
{
"from": "../comfyui-deps",
"to": "comfyui-deps"
"to": "comfyui-deps",
"filter": ["pyproject.toml", "pyproject-flexible-venv.toml", "uv.lock", "custom_nodes/**/*.py"]
},
{
"from": "../build/resources/7zr.exe",
Expand Down
91 changes: 80 additions & 11 deletions WebUI/build/scripts/fetch-external-resources.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
* Uses built-in fetch API and fixed directory structure
*/

import { existsSync, mkdirSync, createWriteStream, renameSync } from 'fs'
import {
existsSync,
mkdirSync,
createWriteStream,
renameSync,
readFileSync,
writeFileSync,
rmSync,
} from 'fs'
import { pipeline } from 'stream/promises'
import { getBuildPaths } from './build-paths.mts'
import path, { normalize } from 'path'
Expand All @@ -31,6 +39,34 @@ interface DownloadResult {
error?: string
}

/**
* Records, per resource key, the exact pinned URL that was last successfully
* downloaded. The version lives in the URL (e.g. `.../26.01/7zr.exe`), so
* comparing the recorded URL against the currently pinned one tells us whether
* a cached resource is stale. Stored alongside the resources; the whole
* directory is gitignored, so this is purely local build state.
*/
type ResourceManifest = Record<string, string>

const MANIFEST_PATH = path.join(buildPaths.resourcesDir, '.resource-versions.json')

function readManifest(): ResourceManifest {
try {
if (existsSync(MANIFEST_PATH)) {
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8')) as ResourceManifest
}
} catch (error) {
console.warn(
`⚠️ Could not read resource manifest (${MANIFEST_PATH}); treating all resources as outdated: ${error}`,
)
}
return {}
}

function writeManifest(manifest: ResourceManifest): void {
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2))
}

/**
* Get base filename from URL
*/
Expand Down Expand Up @@ -68,17 +104,35 @@ async function downloadFile(url: string, targetPath: string): Promise<DownloadRe
}

/**
* Download file if not already present
* Download a resource unless a cached copy at the pinned version already exists.
*
* The cached archive's basename is constant across versions (e.g.
* `7z2601-linux-x64.tar.xz`, `uv-...tar.gz`), so an existence check alone would
* happily reuse an archive from a previous, now-outdated pin. We therefore also
* require the manifest to record the same URL; on a version bump we drop the
* stale cache and re-fetch the exact pinned version.
*/
async function downloadFileIfNotPresent(url: string): Promise<DownloadResult> {
async function downloadFileIfNeeded(
key: string,
url: string,
manifest: ResourceManifest,
): Promise<DownloadResult> {
const fileName = getBaseFileName(url)
const expectedFilePath = path.join(buildPaths.tmpDir, fileName)
const upToDate = manifest[key] === url

if (existsSync(expectedFilePath)) {
console.log(`⏭️ Skipping ${url} - ${expectedFilePath} already exists`)
if (upToDate && existsSync(expectedFilePath)) {
console.log(`⏭️ Skipping ${key} - cached copy already at pinned version (${url})`)
return { url, filePath: expectedFilePath, success: true }
}

if (!upToDate && existsSync(expectedFilePath)) {
console.log(
`♻️ ${key}: pinned version changed (${manifest[key] ?? 'none'} → ${url}); re-downloading`,
)
rmSync(expectedFilePath)
}

return await downloadFile(url, expectedFilePath)
}

Expand Down Expand Up @@ -240,14 +294,21 @@ async function main(): Promise<void> {
// wheels (e.g. insightface) compile during backend setup.
ensureLinuxBuildToolchain()

// Download all required files
const downloads = await Promise.all([
downloadFileIfNotPresent(buildPaths.resourceUrls.uv),
downloadFileIfNotPresent(buildPaths.resourceUrls.sevenZipExe),
// Resources to fetch, each with a stable key used to track its installed
// version in the manifest.
const resourceList = [
{ key: 'uv', url: buildPaths.resourceUrls.uv },
{ key: 'sevenZip', url: buildPaths.resourceUrls.sevenZipExe },
...(buildPaths.resourceUrls.xpuSmiWinZip
? [downloadFileIfNotPresent(buildPaths.resourceUrls.xpuSmiWinZip)]
? [{ key: 'xpuSmi', url: buildPaths.resourceUrls.xpuSmiWinZip }]
: []),
])
]

// Download all required files, re-fetching any whose pinned version changed.
const manifest = readManifest()
const downloads = await Promise.all(
resourceList.map((resource) => downloadFileIfNeeded(resource.key, resource.url, manifest)),
)

// Check for any download failures
const failures = downloads.filter((result) => !result.success)
Expand Down Expand Up @@ -333,6 +394,14 @@ async function main(): Promise<void> {
}
}

// Record the pinned version of every resource only after a fully successful
// fetch+extract, so a failed run never marks a resource as up-to-date.
for (const resource of resourceList) {
manifest[resource.key] = resource.url
}
writeManifest(manifest)
console.log(`📝 Recorded resource versions in ${MANIFEST_PATH}`)

console.log('✅ All Python package resources fetched successfully!')
console.log(`📂 Resources available in: ${buildPaths.resourcesDir}`)
} catch (error) {
Expand Down
145 changes: 105 additions & 40 deletions WebUI/electron/subprocesses/comfyUIBackendService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,21 +513,13 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {
async getCurrentVersion(): Promise<string | undefined> {
try {
try {
const tag = await this.git.run(
['-C', this.serviceDir, 'describe', '--tags', '--exact-match'],
{},
this.serviceDir,
)
const tag = await this.gitRun(['describe', '--tags', '--exact-match'])
const t = tag.trim()
if (t) return normalizeComfyUiRef(t)
} catch {
/* not exactly on a tag */
}
const hash = await this.git.run(
['-C', this.serviceDir, 'rev-parse', '--short', 'HEAD'],
{},
this.serviceDir,
)
const hash = await this.gitRun(['rev-parse', '--short', 'HEAD'])
return normalizeComfyUiRef(hash.trim())
} catch (e) {
this.appLogger.error(`failed to get comfyUI version: ${e}`, this.name)
Expand Down Expand Up @@ -576,18 +568,65 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {
await fs.promises.writeFile(markerPath, JSON.stringify(marker, null, 2), 'utf-8')
}

// Every ComfyUI git command runs against its own checkout: `-C serviceDir` plus
// serviceDir as the working directory. This collapses that repeated boilerplate.
private gitRun(args: string[]): Promise<string> {
return this.git.run(['-C', this.serviceDir, ...args], {}, this.serviceDir)
}

// True only if `serviceDir` is the top level of its OWN git repository.
// This is the critical safety check: `git -C <dir>` walks upward to find a
// `.git`, so if ComfyUI's own `.git` is missing or broken (e.g. an empty
// leftover directory), git resolves to the parent AI-Playground repo and any
// mutating command (fetch/checkout) would corrupt it. An `existsSync('.git')`
// check is NOT sufficient — an empty `.git` dir passes it but is not a repo.
private async isOwnGitRepo(): Promise<boolean> {
try {
const top = (await this.gitRun(['rev-parse', '--show-toplevel'])).trim()
const norm = (p: string) =>
process.platform === 'win32' ? path.resolve(p).toLowerCase() : path.resolve(p)
return norm(top) === norm(this.serviceDir)
} catch {
return false
}
}

// Switch an existing ComfyUI checkout to `requested` in place (fetch + checkout)
// instead of deleting and re-cloning. Deletion is fragile on Windows (locked
// files from a loaded .venv, IDE indexing, read-only .git objects) and was the
// source of repeated EPERM failures; an in-place update touches only tracked
// files and never removes the directory.
private async updateExistingCheckout(requested: string): Promise<boolean> {
// Hard safety gate: never run mutating git unless this dir is its own repo.
if (!(await this.isOwnGitRepo())) {
return false
}
try {
// Fetch from the canonical HTTPS URL rather than the `origin` remote: an
// existing checkout's origin may be configured as SSH (git@github.com:...),
// which fails with "Permission denied (publickey)" in this environment.
await this.gitRun(['fetch', '--force', '--tags', this.remoteUrl])
try {
// Best effort: also fetch the ref directly (covers branches/commits not
// reachable via tags). Tags already arrived above, so ignore failures.
await this.gitRun(['fetch', '--force', this.remoteUrl, requested])
} catch {
/* ignore */
}
// --force discards local changes to tracked files (e.g. pyproject.toml edited
// by ComfyUI-Manager) so the checkout cannot be blocked.
await this.gitRun(['checkout', '--force', requested])
return await this.checkoutMatchesRevision(requested)
} catch (e) {
this.appLogger.info(`in-place ComfyUI update to ${requested} failed: ${e}`, this.name)
return false
}
}

private async checkoutMatchesRevision(requested: string): Promise<boolean> {
try {
const want = (
await this.git.run(
['-C', this.serviceDir, 'rev-parse', `${requested}^{commit}`],
{},
this.serviceDir,
)
).trim()
const head = (
await this.git.run(['-C', this.serviceDir, 'rev-parse', 'HEAD'], {}, this.serviceDir)
).trim()
const want = (await this.gitRun(['rev-parse', `${requested}^{commit}`])).trim()
const head = (await this.gitRun(['rev-parse', 'HEAD'])).trim()
return want.toLowerCase() === head.toLowerCase()
} catch {
return false
Expand All @@ -596,20 +635,12 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {

private async restoreComfyUiPyprojectAndLockFromHead(): Promise<void> {
try {
await this.git.run(
['-C', this.serviceDir, 'checkout', 'HEAD', '--', 'pyproject.toml'],
{},
this.serviceDir,
)
await this.gitRun(['checkout', 'HEAD', '--', 'pyproject.toml'])
} catch {
this.appLogger.info('restore pyproject.toml from HEAD skipped (missing or failed)', this.name)
}
try {
await this.git.run(
['-C', this.serviceDir, 'checkout', 'HEAD', '--', 'uv.lock'],
{},
this.serviceDir,
)
await this.gitRun(['checkout', 'HEAD', '--', 'uv.lock'])
} catch {
this.appLogger.info('restore uv.lock from HEAD skipped (missing or failed)', this.name)
}
Expand Down Expand Up @@ -752,24 +783,58 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {
return false
}

try {
const matches = await this.checkoutMatchesRevision(this.revision)
if (matches) {
this.appLogger.info('comfyUI already cloned at requested revision, skipping', this.name)
// Preferred path: the directory is its own git repo, so update it in place
// (no deletion → no EPERM from locked .venv DLLs / IDE indexing / read-only
// .git objects). This is also the only path that mutates git, and it is
// gated on isOwnGitRepo() so it can never touch the parent AI-Playground repo.
if (await this.isOwnGitRepo()) {
if (await this.checkoutMatchesRevision(this.revision)) {
this.appLogger.info('comfyUI already at requested revision, skipping', this.name)
return true
}
this.appLogger.info(
`ComfyUI checkout does not match requested revision ${this.revision}. Removing...`,
`Updating ComfyUI checkout to requested revision ${this.revision} in place`,
this.name,
)
if (await this.updateExistingCheckout(this.revision)) {
this.appLogger.info(`comfyUI updated to ${this.revision} in place`, this.name)
return true
}
this.appLogger.warn(
'In-place ComfyUI update failed; falling back to remove + fresh clone',
this.name,
)
throw new Error('Version mismatch')
} catch (_e) {
} else {
this.appLogger.warn(
`ComfyUI directory ${this.serviceDir} is not its own git repository; removing for a fresh clone`,
this.name,
)
}

// Fallback: wipe the directory so the caller re-clones. Retry a few times —
// after the backend process is killed Windows can take a moment to release
// directory handles. Surface a clear, actionable error if it ultimately
// cannot be removed rather than letting `git clone` fail later with a
// confusing "destination path already exists" message.
let lastError: unknown
for (let attempt = 0; attempt < 5; attempt++) {
try {
filesystem.removeSync(this.serviceDir)
} finally {
return false
lastError = undefined
break
} catch (removeError) {
lastError = removeError
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
if (lastError) {
throw new Error(
`Failed to remove existing ComfyUI directory ${this.serviceDir} for reinstall. ` +
`Close any program holding files there (a running ComfyUI, an IDE indexing the ` +
`folder, or a terminal/Explorer window inside it) and try again. Cause: ${lastError}`,
)
}
return false
}

const setupComfyUiBaseService = async (): Promise<void> => {
Expand All @@ -778,7 +843,7 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {
this.appLogger.info('comfyUI already cloned, skipping', this.name)
} else {
await this.git.run(['clone', this.remoteUrl, this.serviceDir])
await this.git.run(['-C', this.serviceDir, 'checkout', this.revision], {}, this.serviceDir)
await this.gitRun(['checkout', this.revision])
}

const comfyUIDepsDir = path.join(aipgBaseDir, 'comfyui-deps')
Expand Down
6 changes: 1 addition & 5 deletions WebUI/electron/subprocesses/comfyUiRevision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ export type ComfyUiDepsMarker = {
* Normalize a user or git ref for comparison (trim, lowercase hex hashes).
*/
export function normalizeComfyUiRef(ref: string): string {
const t = ref.trim()
if (/^[0-9a-f]{7,40}$/i.test(t)) {
return t.toLowerCase()
}
return t.toLowerCase()
return ref.trim().toLowerCase()
}

/**
Expand Down
Loading
Loading