gitbutlerapp/gitbutler · error

Invalid response: ${JSON.stringify(result)}

Error message

Invalid response: ${JSON.stringify(result)}

What it means

Thrown during hunk-based discard when the matched Modification/Rename change has a typechange flag, meaning the path changed blob kind between the previous state and the worktree (file to symlink, file to submodule/gitlink, etc.). Line hunks only make sense between two text blobs of the same kind, so the library refuses and asks for whole-file mode.

Source

Thrown at apps/desktop/src/lib/ai/ollamaClient.ts:84

	 * Sends a chat message to the LLM model and returns the response.
	 *
	 * @param messages - An array of LLMChatMessage objects representing the chat messages.
	 * @param options - Optional LLMRequestOptions object for specifying additional options.
	 * @returns A Promise that resolves to an LLMResponse object representing the response from the LLM model.
	 */
	private async chat(
		messages: Prompt,
		options?: OllamaRequestOptions,
	): Promise<OllamaChatResponse> {
		const result = await this.ollama.chat({
			model: this.modelName,
			messages,
			stream: false,
			options,
		});

		if (!isOllamaChatResponse(result)) {
			throw new Error("Invalid response: " + JSON.stringify(result));
		}

		return result;
	}
}

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Discard the whole file instead: send the DiffSpec with empty hunk_headers, which restores the previous state including its object kind
  2. Recompute worktree changes so the typechange is visible in the UI and line-level discard is not offered
  3. If the typechange is unintended (e.g. accidental symlink), resolve it in the worktree first, then redo the hunk discard

Example fix

// before
let specs = vec![DiffSpec { path, previous_path: None, hunk_headers: selected_hunks }];

// after: whole-file restore handles the kind change
let specs = vec![DiffSpec { path, previous_path: None, hunk_headers: Vec::new() }];
Defensive patterns

Strategy: validation

Validate before calling

let wt = but_core::diff::worktree_changes(repo)?;
for spec in &changes {
    if spec.hunk_headers.is_empty() { continue; }
    if let Some(change) = wt.changes.iter().find(|c| c.path == spec.path) {
        if let but_core::TreeStatus::Modification { flags: Some(f), .. } = &change.status {
            assert!(!f.is_typechange(), "{} is type-changed: use whole-file mode", spec.path);
        }
    }
}

Type guard

fn is_typechange(status: &but_core::TreeStatus) -> bool {
    matches!(status,
        but_core::TreeStatus::Modification { flags: Some(f), .. }
        | but_core::TreeStatus::Rename { flags: Some(f), .. } if f.is_typechange())
}

Try / catch

if let Err(err) = discard_workspace_changes(repo, specs, ctx) {
    if err.to_string().contains("Type-changed") {
        // fall back: same spec with hunk_headers cleared
    } else { return Err(err); }
}

Prevention

When it happens

Trigger: discard_workspace_changes with a spec containing hunk_headers where the worktree change is TreeStatus::Modification or Rename whose flags satisfy f.is_typechange() - e.g. the user replaced a regular file with a symlink, or a directory became a submodule, then tried to discard selected lines of it.

Common situations: Editor/tooling replacing files with symlinks (dependency dirs, nix-style linking), git submodules added where a file used to be, or a stale diff computed before the type change. Windows dev drives or core.symlinks toggles can also surface file<->symlink typechanges.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/a29148e3d92661a0. Report an issue: GitHub.