windmill-labs/windmill · error

file only applies to multi-file apps; ${type} "${path}" diff

Error message

file only applies to multi-file apps; ${type} "${path}" diffs as a single document — call again without file.

What it means

The `file` argument of the copilot diff tool narrows a diff to one file of a multi-file item (apps). When the computed diff produced no `files` map — the item is a single-document kind — passing `file` is invalid and the tool throws this corrective error telling the agent to retry without it. It is an argument-surface misuse guard, not a data failure.

Source

Thrown at frontend/src/lib/components/copilot/chat/global/core.ts:6657

		}
	}

	const changedFileCount = files ? Object.keys(files).length : 0
	if (!patch && changedFileCount === 0) {
		// A secret's sides are masked on both ends — an empty patch cannot prove
		// the value is unchanged.
		const message = valueUncomparable
			? `No visible changes for ${type} "${path}" — but a secret's value cannot be compared and may have been updated in the draft.`
			: `Draft matches the deployed version of ${type} "${path}" — no changes.`
		toolCallbacks.setToolStatus(toolId, { content: message, result: message })
		return flushCaveat + message
	}

	const header = noDeployed
		? `${type} "${path}" has no deployed version yet — the entire draft is new.\n\n`
		: `Draft changes vs deployed for ${type} "${path}":\n\n`
	if (args.file !== undefined && !files) {
		throw new Error(
			`file only applies to multi-file apps; ${type} "${path}" diffs as a single document — call again without file.`
		)
	}
	const body = files
		? renderEntryFiles(files, patch, args)
		: windowPatchBody(patch, args.offset ?? 0, args.limit ?? DIFF_READ_DEFAULT_LINES)
	const result = flushCaveat + header + body
	toolCallbacks.setToolStatus(toolId, {
		content: `Draft vs deployed diff for "${path}"`,
		result
	})
	return result
}

// Body of an item read for a multi-file app: one file's patch when `file` is
// given, otherwise the per-file summary plus config changes.
function renderEntryFiles(
	files: Record<string, DiffFileView>,

View on GitHub (pinned to e474e8803c)

Solutions

  1. Retry the same diff call without the `file` argument
  2. If you need per-file output for a multi-file app, confirm the type is 'app' or 'raw_app' before passing `file`

Example fix

// before
diff({ type: 'script', path: 'f/build', file: 'main.ts' })
// after
diff({ type: 'script', path: 'f/build' })
Defensive patterns

Strategy: validation

Validate before calling

if (args.file !== undefined && !['app', 'raw_app'].includes(args.type)) {
  throw new Error('file is only valid for app diffs')
}

Type guard

function supportsFileFilter(args: { type: string; file?: string }): boolean {
  return args.file === undefined || args.type === 'app' || args.type === 'raw_app'
}

Try / catch

try {
  return await diffItem(args)
} catch (e) {
  if (String(e?.message).startsWith('file only applies to multi-file apps')) {
    return await diffItem({ ...args, file: undefined })
  }
  throw e
}

Prevention

When it happens

Trigger: Calling diff with `args.file` set while the item is a single-document type (script, flow, variable, resource, etc.) or the diff entry has no files map, i.e. `args.file !== undefined && !files`.

Common situations: An agent habitually includes `file` from a previous app-diff call; a template call copied from an app example applied to a script; confusing multi-file apps with single-file scripts.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/89955ac1d61b0b47. Report an issue: GitHub.