Sunday, May 17, 2026

AI: export Google AI Studio chats

 in google ai studio, is there a way to copy/export whole conversation?


Currently, Google AI Studio does not have a native "Export" or "Download" button for saving your conversations in readable formats like PDF or Markdown.

Furthermore, simply hitting Ctrl+A or Cmd+A to copy the page manually usually fails on longer chats. This is because AI Studio uses "virtual scrolling," meaning the interface only loads the chat bubbles currently visible on your screen.

However, there are three practical ways to export or copy your entire conversation, depending on how you need the data:

1. The Best Method: Use a Browser Extension (Third-Party)

Because this is a major pain point for developers, the community has created free Chrome extensions specifically designed to bypass the virtual scrolling limitation and extract the full chat.

  • Google AI Studio Exporter or Chat Exporter for Google AI Studio: These are free extensions in the Chrome Web Store. When clicked, they automatically scroll through your entire chat to capture every message and let you download the conversation as a clean Markdown (.md), PDF, or plain text file.
  • Gemini to PDF: Another popular community extension that adds a "Save Chat" button to export your conversations preserving formatting, code blocks, and tables.

2. The Quick Native Workaround: The "Get Code" Button

If you don't want to install an extension and just need the raw text of the entire conversation, you can use the built-in code generator:

  1. Click the Get Code button at the top right of the AI Studio interface.
  2. Switch the tab to cURL, JSON, or Python.
  3. Inside the code snippet, look for the contents or history array. This contains your entire back-and-forth conversation in a structured text/JSON format.
  4. You can copy this entire block of code to your clipboard. While it includes coding syntax, it ensures no part of the conversation is cut off.

3. The Backup Method: Download the JSON from Google Drive

If you have "Auto-save" enabled, Google AI Studio automatically backs up your chats to your Google Drive.

  1. Go to your Google Drive and locate the saved AI Studio file (it usually has the blue/white AI Studio butterfly icon).
  2. If you double-click it, it will just reopen AI Studio.
  3. Instead, right-click the file and select Download.
  4. This will download a .json file containing the complete chat history, prompt configurations, and system instructions, which you can open in any text editor or parse with a script.


here are simple TS scripts, to run with Bun.js to convert from JSON to MD format

first one by Gemini

// convert-simple.ts
import { parse, format } from 'path';

const TITLE_MAX_LEN = 60;

async function run() {
  const inputFile = process.argv[2];
  if (!inputFile) {
    console.error("❌ Usage: bun run convert-simple.ts <path-to-json-file>");
    process.exit(1);
  }

  const parsedPath = parse(inputFile);
  const outputFile = format({ dir: parsedPath.dir || '.', name: parsedPath.name, ext: '.md' });

  console.log(`Reading JSON from: ${inputFile}...`);
 
  // Bun.file().json() is highly optimized in C++ under the hood
  const data = await Bun.file(inputFile).json();
  const chunks = data.chunkedPrompt?.chunks || [];

  let mdContent = "";
  let promptCount = 0;

  for (const chunk of chunks) {
    if (chunk.role === 'user' && chunk.text) {
      promptCount++;
     
      let title = chunk.text.replace(/[\r\n]+/g, ' ').trim();
      if (title.length > TITLE_MAX_LEN) title = title.substring(0, TITLE_MAX_LEN) + '...';
     
      mdContent += `## ${promptCount}. ${title}\n\n${chunk.text}\n\n---\n\n`;
    }
    else if (chunk.role === 'model' && chunk.finishReason === 'STOP' && chunk.text) {
      mdContent += `${chunk.text}\n\n\n`;
    }
  }

  await Bun.write(outputFile, mdContent);
  console.log(`✅ Done! Exported ${promptCount} QA pairs to ${outputFile}`);
}

run();

and here is one from Sonnet... both work, Sonnet version is slightly more efficient, while more code; but Gemini also generated most efficient verison

#!/usr/bin/env bun
// Convert a Google AI Studio chat export (.json) to markdown.
//
// Usage:
//   bun src/json-to-md.ts <input.json> [--preview <n>] [--out <path>]
//
// Each user prompt becomes a "## N. <preview>…" section, followed by the full
// prompt, a `---`, and the paired model response (the next model chunk whose
// `finishReason` is "STOP"). Intermediate "thought" chunks are skipped.

import { basename, dirname, extname, join } from "node:path";

type Chunk = {
  text?: string;
  role?: "user" | "model";
  finishReason?: string;
  isThought?: boolean;
};

type Export = {
  chunkedPrompt?: { chunks?: Chunk[] };
};

function parseArgs(argv: string[]) {
  const args = argv.slice(2);
  let input: string | undefined;
  let preview = 60;
  let out: string | undefined;

  for (let i = 0; i < args.length; i++) {
    const a = args[i]!;
    if (a === "--preview" || a === "-n") {
      preview = Number(args[++i]);
      if (!Number.isFinite(preview) || preview <= 0) {
        throw new Error("--preview must be a positive number");
      }
    } else if (a === "--out" || a === "-o") {
      out = args[++i];
    } else if (a === "-h" || a === "--help") {
      console.log(
        "Usage: bun src/json-to-md.ts <input.json> [--preview <n>] [--out <path>]",
      );
      process.exit(0);
    } else if (!input) {
      input = a;
    } else {
      throw new Error(`Unexpected argument: ${a}`);
    }
  }

  if (!input) throw new Error("Missing input JSON path");
  return { input, preview, out };
}

function previewText(s: string, n: number): string {
  const oneLine = s.replace(/\s+/g, " ").trim();
  return oneLine.length <= n ? oneLine : oneLine.slice(0, n).trimEnd() + "…";
}

async function main() {
  const { input, preview, out } = parseArgs(process.argv);

  // Bun.file streams from disk; .json() parses without loading the raw text
  // twice. For multi-GB exports a true streaming JSON parser would be needed,
  // but chat exports are well under that.
  const data = (await Bun.file(input).json()) as Export;
  const chunks = data.chunkedPrompt?.chunks ?? [];

  const lines: string[] = [];
  let userCount = 0;
  let currentUser: string | null = null;

  for (const chunk of chunks) {
    if (chunk.role === "user") {
      if (currentUser !== null) {
        // Previous user prompt had no STOP response — emit it anyway so
        // nothing is silently dropped.
        userCount++;
        lines.push(`## ${userCount}. ${previewText(currentUser, preview)}`);
        lines.push("");
        lines.push(currentUser);
        lines.push("");
        lines.push("---");
        lines.push("");
        lines.push("_(no model response)_");
        lines.push("");
      }
      currentUser = chunk.text ?? "";
      continue;
    }

    if (
      chunk.role === "model" &&
      chunk.finishReason === "STOP" &&
      !chunk.isThought &&
      currentUser !== null
    ) {
      userCount++;
      lines.push(`## ${userCount}. ${previewText(currentUser, preview)}`);
      lines.push("");
      lines.push(currentUser);
      lines.push("");
      lines.push("---");
      lines.push("");
      lines.push(chunk.text ?? "");
      lines.push("");
      currentUser = null;
    }
  }

  if (currentUser !== null) {
    userCount++;
    lines.push(`## ${userCount}. ${previewText(currentUser, preview)}`);
    lines.push("");
    lines.push(currentUser);
    lines.push("");
    lines.push("---");
    lines.push("");
    lines.push("_(no model response)_");
    lines.push("");
  }

  const outPath =
    out ??
    join(dirname(input), basename(input, extname(input)) + ".md");

  await Bun.write(outPath, lines.join("\n"));
  console.log(`Wrote ${userCount} section(s) to ${outPath}`);
}

main().catch((err) => {
  console.error(err instanceof Error ? err.message : err);
  process.exit(1);
});


for very large JSON files (> 100 MB), here is "streaming" version from Gemini

import { createReadStream, createWriteStream } from 'fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';
import { parse, format } from 'path';

// --- CONFIGURATION ---
const TITLE_MAX_LEN = 60;

async function processChatLog(inputFile: string) {
  // Generate output filename (same name, .md extension)
  const parsedPath = parse(inputFile);
  const outputFile = format({
    dir: parsedPath.dir || '.',
    name: parsedPath.name,
    ext: '.md'
  });

  const outStream = createWriteStream(outputFile, { flags: 'w' });
  let promptCount = 0;

  console.log(`Streaming JSON from: ${inputFile}...`);
  console.log(`Writing Markdown to: ${outputFile}...`);

  // Build the streaming pipeline targeting only the 'chunkedPrompt.chunks' array
  const jsonStream = createReadStream(inputFile)
    .pipe(parser())
    .pipe(pick({ filter: 'chunkedPrompt.chunks' }))
    .pipe(streamArray());

  // Process chunks one by one as they stream into memory
  for await (const data of jsonStream) {
    const chunk = data.value;

    if (chunk.role === 'user' && chunk.text) {
      promptCount++;
     
      // Clean up the title: replace newlines with spaces and truncate
      let title = chunk.text.replace(/[\r\n]+/g, ' ').trim();
      if (title.length > TITLE_MAX_LEN) {
        title = title.substring(0, TITLE_MAX_LEN) + '...';
      }
     
      // Write the Section Header
      outStream.write(`## ${promptCount}. ${title}\n\n`);
     
      // Write the full multi-line user prompt
      outStream.write(`${chunk.text}\n\n`);
     
      // Write the separator
      outStream.write(`---\n\n`);
    }
    else if (chunk.role === 'model' && chunk.finishReason === 'STOP' && chunk.text) {
      // Write the final model response (ignoring "isThought" chunks)
      outStream.write(`${chunk.text}\n\n\n`);
    }
  }

  outStream.end();
  console.log(`\n✅ Done! Successfully exported ${promptCount} QA pairs.`);
}

const fileArg = process.argv[2];
if (!fileArg) {
  console.error("❌ Usage: bun run convert.ts <path-to-json-file>");
  process.exit(1);
}

processChatLog(fileArg).catch((err) => {
  console.error("An error occurred during conversion:", err);
});

No comments: