Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@

# Strip [tool.uv.sources] from pyproject.toml so git status stays clean
# Requires: ./scripts/install-hooks.sh (configures the filter in git config)
pyproject.toml filter=strip-uv-sources

# Use bd merge for beads JSONL files
.beads/issues.jsonl merge=beads
21 changes: 17 additions & 4 deletions frontend/src/components/EnvironmentSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
<div v-if="!isManaged" class="warning-banner">
⚠️ Not in ComfyGit workspace. Environment switching is disabled.
</div>
<div v-else-if="orchestratorActive && !isSupervised" class="warning-banner">
⚠️ An orchestrator is managing this workspace from another ComfyUI instance.
Switch environments from that instance, or stop the orchestrator first.
</div>

<div v-else class="env-selector">
<div v-if="isManaged" class="env-selector">
<select
v-model="selectedEnv"
:disabled="switching || environments.length === 0"
Expand Down Expand Up @@ -86,10 +90,13 @@ const switchStatus = ref({
})
const criticalFailure = ref<any>(null)
const isManaged = ref(true)
const orchestratorActive = ref(false)
const isSupervised = ref(false)

// Computed
const canSwitch = computed(() => {
return isManaged.value &&
!(orchestratorActive.value && !isSupervised.value) &&
selectedEnv.value !== currentEnv.value &&
!switching.value &&
environments.value.length > 0
Expand All @@ -103,9 +110,12 @@ async function loadEnvironments() {
try {
const response = await service.listEnvironments()
environments.value = response.environments || []
currentEnv.value = response.current || ''
selectedEnv.value = currentEnv.value
const current = response.current || environments.value.find((env) => env.is_current)?.name || ''
currentEnv.value = current
selectedEnv.value = current
isManaged.value = response.is_managed !== false
orchestratorActive.value = response.orchestrator_active === true
isSupervised.value = response.is_supervised === true
} catch (error) {
console.error('Failed to load environments:', error)
emit('toast', 'Failed to load environments', 'error')
Expand Down Expand Up @@ -135,7 +145,10 @@ function startStatusPolling() {

statusPollInterval = window.setInterval(async () => {
try {
const status = await service.getSwitchStatus()
const status = await service.getSwitchProgress()
if (!status) {
return
}

switchStatus.value = status

Expand Down
161 changes: 154 additions & 7 deletions frontend/src/components/ExportBlockedModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<BaseModal
title="Cannot Export"
:title="hasCommittableIssues ? 'Commit & Export' : 'Cannot Export'"
size="md"
@close="$emit('close')"
>
Expand All @@ -16,7 +16,9 @@
<div class="error-summary">
<h3 class="error-title">Export blocked</h3>
<p class="error-description">
The following issues must be resolved before exporting.
{{ hasCommittableIssues
? 'Commit your changes to proceed with export.'
: 'The following issues must be resolved before exporting.' }}
</p>
</div>
</div>
Expand Down Expand Up @@ -58,40 +60,133 @@
</div>
</div>
</div>

<!-- Commit Section (only when there are committable issues) -->
<div v-if="hasCommittableIssues" class="commit-section">
<!-- Allow issues checkbox (only when unresolved issues also present) -->
<div v-if="hasUnresolvedIssues" class="issues-warning">
<div class="warning-header">
<span class="warning-icon-badge">!</span>
<span class="warning-label">Some workflow issues cannot be fixed by committing</span>
</div>
<BaseCheckbox v-model="allowIssues" class="allow-issues-toggle">
Commit anyway (ignore issues)
</BaseCheckbox>
</div>

<BaseTextarea
v-model="message"
placeholder="Describe your changes..."
:disabled="isCommitting || isBlockedByIssues"
:rows="3"
:submit-on-enter="true"
@submit="handleCommitAndExport"
@ctrl-enter="handleCommitAndExport"
/>

<div v-if="commitError" class="commit-error">
{{ commitError }}
</div>
</div>
</div>
</template>

<template #footer>
<BaseButton variant="primary" @click="$emit('close')">
Understood
</BaseButton>
<template v-if="hasCommittableIssues">
<BaseButton variant="secondary" @click="$emit('close')">
Cancel
</BaseButton>
<BaseButton
:variant="allowIssues ? 'danger' : 'primary'"
:disabled="!canCommit"
:loading="isCommitting"
@click="handleCommitAndExport"
>
{{ isCommitting ? 'Committing...' : allowIssues ? 'Force Commit & Export' : 'Commit & Export' }}
</BaseButton>
</template>
<template v-else>
<BaseButton variant="primary" @click="$emit('close')">
Understood
</BaseButton>
</template>
</template>
</BaseModal>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
import { ref, reactive, computed } from 'vue'
import BaseModal from './base/BaseModal.vue'
import BaseButton from './base/BaseButton.vue'
import BaseTextarea from './base/BaseTextarea.vue'
import BaseCheckbox from './base/BaseCheckbox.vue'
import { useComfyGitService } from '@/composables/useComfyGitService'
import type { ExportBlockingIssue } from '@/types/comfygit'

const props = defineProps<{
issues: ExportBlockingIssue[]
}>()

defineEmits<{
const emit = defineEmits<{
close: []
committed: []
}>()

const { commit } = useComfyGitService()

const message = ref('')
const isCommitting = ref(false)
const allowIssues = ref(false)
const commitError = ref('')
const showAllDetails = reactive<Record<number, boolean>>({})

const hasCommittableIssues = computed(() =>
props.issues.some(i => i.type === 'uncommitted_workflows' || i.type === 'uncommitted_git_changes')
)

const hasUnresolvedIssues = computed(() =>
props.issues.some(i => i.type === 'unresolved_issues')
)

const isBlockedByIssues = computed(() =>
hasUnresolvedIssues.value && !allowIssues.value
)

const canCommit = computed(() =>
hasCommittableIssues.value && message.value.trim() !== '' && !isCommitting.value && !isBlockedByIssues.value
)

function visibleDetails(index: number): string[] {
const issue = props.issues[index]
if (showAllDetails[index] || issue.details.length <= 3) {
return issue.details
}
return issue.details.slice(0, 3)
}

async function handleCommitAndExport() {
if (!canCommit.value) return

isCommitting.value = true
commitError.value = ''

try {
const res = await commit(message.value.trim(), allowIssues.value)

if (res.status === 'success') {
emit('committed')
} else if (res.status === 'blocked') {
const issuesList = res.issues?.map(i => `${i.name}: ${i.issue}`).join('; ') || 'Unknown issues'
commitError.value = `Commit blocked: ${issuesList}`
} else {
commitError.value = res.message || 'Commit failed'
}
} catch (err) {
commitError.value = err instanceof Error ? err.message : 'Commit failed'
} finally {
isCommitting.value = false
}
}
</script>

<style scoped>
Expand Down Expand Up @@ -192,4 +287,56 @@ function visibleDetails(index: number): string[] {
color: var(--cg-color-text-muted);
font-style: italic;
}

/* Commit Section */
.commit-section {
display: flex;
flex-direction: column;
gap: var(--cg-space-3);
padding-top: var(--cg-space-3);
border-top: 1px solid var(--cg-color-border);
}

.issues-warning {
background: var(--cg-color-warning-muted);
border: 1px solid var(--cg-color-warning);
border-radius: var(--cg-radius-md);
padding: var(--cg-space-3);
}

.warning-header {
display: flex;
align-items: center;
gap: var(--cg-space-2);
margin-bottom: var(--cg-space-2);
font-size: var(--cg-font-size-xs);
color: var(--cg-color-text-secondary);
}

.warning-icon-badge {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: var(--cg-color-warning);
color: var(--cg-color-bg-primary);
border-radius: 50%;
font-size: var(--cg-font-size-xs);
font-weight: var(--cg-font-weight-bold);
flex-shrink: 0;
}

.allow-issues-toggle {
font-size: var(--cg-font-size-xs);
}

.commit-error {
font-size: var(--cg-font-size-xs);
color: var(--cg-color-error);
padding: var(--cg-space-2);
background: var(--cg-color-error-muted);
border: 1px solid var(--cg-color-error);
border-radius: var(--cg-radius-md);
}
</style>
30 changes: 30 additions & 0 deletions frontend/src/components/ExportSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
v-if="showBlockedModal && validationResult"
:issues="validationResult.blocking_issues"
@close="showBlockedModal = false"
@committed="handleBlockedCommitted"
/>

<!-- Export Warnings Modal -->
Expand Down Expand Up @@ -215,6 +216,35 @@ async function handleExportConfirmed() {
await executeExport()
}

async function handleBlockedCommitted() {
showBlockedModal.value = false

// Re-validate after successful commit
isValidating.value = true
try {
const result = await validateExport()
validationResult.value = result

if (!result.can_export) {
// Still blocked — re-show modal with updated issues
showBlockedModal.value = true
} else if (result.warnings.models_without_sources.length > 0) {
// Warnings only — show warnings modal
showWarningsModal.value = true
} else {
// Clean — auto-export
await executeExport()
}
} catch (err) {
exportResult.value = {
status: 'error',
message: err instanceof Error ? err.message : 'Re-validation failed'
}
} finally {
isValidating.value = false
}
}

async function handleRevalidate() {
try {
const result = await validateExport()
Expand Down
Loading
Loading